test: migrate rpc tests to pytest asyncio
This commit is contained in:
@@ -28,12 +28,14 @@ dev = [
|
|||||||
"basedpyright>=1.39.6",
|
"basedpyright>=1.39.6",
|
||||||
"pytest>=8",
|
"pytest>=8",
|
||||||
# "pytest-sugar>=1",
|
# "pytest-sugar>=1",
|
||||||
|
"pytest-asyncio>=1.4.0",
|
||||||
"ruff>=0.15.15",
|
"ruff>=0.15.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "-p no:cacheprovider"
|
addopts = "-p no:cacheprovider"
|
||||||
pythonpath = ["."]
|
pythonpath = ["."]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
package = true
|
package = true
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -22,311 +21,293 @@ async def _rpc(
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_health_and_capability_methods(tmp_path) -> None:
|
async def test_rpc_health_and_capability_methods(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
health_response = await client.get("/healthz")
|
||||||
health_response = await client.get("/healthz")
|
health = await _rpc(client, "workflow.health", {})
|
||||||
health = await _rpc(client, "workflow.health", {})
|
listed = await _rpc(
|
||||||
listed = await _rpc(
|
client,
|
||||||
client,
|
"workflow.capabilities.list",
|
||||||
"workflow.capabilities.list",
|
{"source_id": "wf.std", "limit": 10},
|
||||||
{"source_id": "wf.std", "limit": 10},
|
|
||||||
)
|
|
||||||
inspected = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.capabilities.inspect",
|
|
||||||
{"qualified_name": "wf.std.constant"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert health_response.status_code == 200
|
|
||||||
assert health_response.json()["status"] == "ok"
|
|
||||||
assert health["result"]["status"] == "ok"
|
|
||||||
assert listed["result"]["capabilities"]
|
|
||||||
assert inspected["result"]["name"] == "wf.std.constant"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_unknown_method_returns_json_rpc_error(tmp_path) -> None:
|
|
||||||
async def scenario() -> 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:
|
|
||||||
payload = await _rpc(client, "workflow.nope", {})
|
|
||||||
|
|
||||||
assert payload["error"]["code"] == -32601
|
|
||||||
assert payload["error"]["message"] == "Method not found"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_app_mounts_configured_rpc_path(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
|
||||||
app = create_rpc_app(server, rpc_path="/workflow-rpc")
|
|
||||||
transport = httpx.ASGITransport(app=app)
|
|
||||||
async with httpx.AsyncClient(
|
|
||||||
transport=transport, base_url="http://test"
|
|
||||||
) as client:
|
|
||||||
response = await client.post(
|
|
||||||
"/workflow-rpc",
|
|
||||||
json={
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "test",
|
|
||||||
"method": "workflow.health",
|
|
||||||
"params": {},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.json()["result"]["status"] == "ok"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_draft_artifact_deployment_lifecycle(tmp_path) -> None:
|
|
||||||
async def scenario() -> 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:
|
|
||||||
draft_ws = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.drafts.create_from_capability",
|
|
||||||
{
|
|
||||||
"workspace_id": "constant_ws",
|
|
||||||
"capability_name": "wf.std.constant",
|
|
||||||
"name": "constant_workflow",
|
|
||||||
"title": "Constant Workflow",
|
|
||||||
"input_map": {},
|
|
||||||
"output_map": {"value": "state.result"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
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(
|
|
||||||
client,
|
|
||||||
"workflow.drafts.validate",
|
|
||||||
{"draft": draft},
|
|
||||||
)
|
|
||||||
compiled_plan = validate_draft["result"]["compiled_plan"]
|
|
||||||
artifact = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.artifacts.save",
|
|
||||||
{
|
|
||||||
"artifact": {
|
|
||||||
"id": "constant_rpc",
|
|
||||||
"version": 1,
|
|
||||||
"kind": "wrapper",
|
|
||||||
"title": "Constant RPC",
|
|
||||||
"input_schema": {"type": "object", "properties": {}},
|
|
||||||
"output_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"result": {"type": "string"}},
|
|
||||||
"required": ["result"],
|
|
||||||
},
|
|
||||||
"outcomes": ["ok"],
|
|
||||||
"required_capabilities": {},
|
|
||||||
"source_bindings": {"wf.std": "wf.std"},
|
|
||||||
"plan": compiled_plan,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
deployment = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.deployments.save",
|
|
||||||
{
|
|
||||||
"deployment": {
|
|
||||||
"id": "constant_rpc.default",
|
|
||||||
"artifact_id": "constant_rpc",
|
|
||||||
"artifact_version": 1,
|
|
||||||
"bindings": [
|
|
||||||
{"logical_source": "wf.std", "concrete_source": "wf.std"}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
validate_deployment = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.deployments.validate",
|
|
||||||
{"deployment_id": "constant_rpc.default"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert draft_ws["result"]["workspace_id"] == "constant_ws"
|
|
||||||
assert validate_draft["result"]["status"] == "valid"
|
|
||||||
assert artifact["result"]["artifact_id"] == "constant_rpc"
|
|
||||||
assert deployment["result"]["deployment_id"] == "constant_rpc.default"
|
|
||||||
assert validate_deployment["result"]["status"] == "runnable"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_artifact_and_deployment_catalog_methods(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
|
||||||
await server.api.create_artifact_from_plan(
|
|
||||||
artifact_id="rpc_lifecycle",
|
|
||||||
version=1,
|
|
||||||
title="RPC Lifecycle",
|
|
||||||
plan=_constant_plan(),
|
|
||||||
outcomes=["ok"],
|
|
||||||
source_bindings={"wf.std": "wf.std"},
|
|
||||||
)
|
)
|
||||||
await server.api.save_deployment(
|
inspected = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.capabilities.inspect",
|
||||||
|
{"qualified_name": "wf.std.constant"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert health_response.status_code == 200
|
||||||
|
assert health_response.json()["status"] == "ok"
|
||||||
|
assert health["result"]["status"] == "ok"
|
||||||
|
assert listed["result"]["capabilities"]
|
||||||
|
assert inspected["result"]["name"] == "wf.std.constant"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_unknown_method_returns_json_rpc_error(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:
|
||||||
|
payload = await _rpc(client, "workflow.nope", {})
|
||||||
|
|
||||||
|
assert payload["error"]["code"] == -32601
|
||||||
|
assert payload["error"]["message"] == "Method not found"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_app_mounts_configured_rpc_path(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
app = create_rpc_app(server, rpc_path="/workflow-rpc")
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/workflow-rpc",
|
||||||
|
json={
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "test",
|
||||||
|
"method": "workflow.health",
|
||||||
|
"params": {},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["result"]["status"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_draft_artifact_deployment_lifecycle(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:
|
||||||
|
draft_ws = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.drafts.create_from_capability",
|
||||||
{
|
{
|
||||||
"id": "rpc_lifecycle.default",
|
"workspace_id": "constant_ws",
|
||||||
"artifact_id": "rpc_lifecycle",
|
"capability_name": "wf.std.constant",
|
||||||
"artifact_version": 1,
|
"name": "constant_workflow",
|
||||||
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
"title": "Constant Workflow",
|
||||||
}
|
"input_map": {},
|
||||||
|
"output_map": {"value": "state.result"},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
app = create_rpc_app(server)
|
draft = {
|
||||||
transport = httpx.ASGITransport(app=app)
|
"name": "rpc_constant",
|
||||||
async with httpx.AsyncClient(
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
transport=transport, base_url="http://test"
|
"state_schema": {
|
||||||
) as client:
|
"type": "object",
|
||||||
listed_artifacts = await _rpc(client, "workflow.artifacts.list", {})
|
"properties": {
|
||||||
inspected_artifact = await _rpc(
|
"result": {"type": "string", "reducer": "wf.std.replace"}
|
||||||
client,
|
|
||||||
"workflow.artifacts.inspect",
|
|
||||||
{"artifact_id": "rpc_lifecycle", "version": 1},
|
|
||||||
)
|
|
||||||
listed_deployments = await _rpc(client, "workflow.deployments.list", {})
|
|
||||||
inspected_deployment = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.deployments.inspect",
|
|
||||||
{"deployment_id": "rpc_lifecycle.default"},
|
|
||||||
)
|
|
||||||
deleted = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.deployments.delete",
|
|
||||||
{"deployment_id": "rpc_lifecycle.default"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert listed_artifacts["result"]["nodes"]
|
|
||||||
assert inspected_artifact["result"]["id"] == "rpc_lifecycle"
|
|
||||||
assert listed_deployments["result"]["deployments"]
|
|
||||||
assert inspected_deployment["result"]["id"] == "rpc_lifecycle.default"
|
|
||||||
assert deleted["result"]["deployment_id"] == "rpc_lifecycle.default"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_draft_workspace_methods(tmp_path) -> None:
|
|
||||||
async def scenario() -> 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:
|
|
||||||
created = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.draft_workspaces.create_from_capability",
|
|
||||||
{
|
|
||||||
"workspace_id": "remote_ws",
|
|
||||||
"capability_name": "wf.std.constant",
|
|
||||||
"name": "remote_constant",
|
|
||||||
"title": "Remote Constant",
|
|
||||||
"input_map": {},
|
|
||||||
"output_map": {"value": "state.result"},
|
|
||||||
},
|
},
|
||||||
)
|
},
|
||||||
listed = await _rpc(client, "workflow.draft_workspaces.list", {})
|
"output_schema": {
|
||||||
fetched = await _rpc(
|
"type": "object",
|
||||||
client,
|
"properties": {"result": {"type": "string"}},
|
||||||
"workflow.draft_workspaces.get",
|
"required": ["result"],
|
||||||
{"workspace_id": "remote_ws"},
|
},
|
||||||
)
|
"start": "constant",
|
||||||
validated = await _rpc(
|
"steps": {
|
||||||
client,
|
"constant": {
|
||||||
"workflow.draft_workspaces.validate",
|
"use": "wf.std.constant",
|
||||||
{"workspace_id": "remote_ws"},
|
"input": [
|
||||||
)
|
{
|
||||||
patched = await _rpc(
|
"value": "hello over rpc",
|
||||||
client,
|
"target": {"root": "local", "parts": ["value"]},
|
||||||
"workflow.draft_workspaces.patch",
|
}
|
||||||
|
],
|
||||||
|
"output": [
|
||||||
|
{
|
||||||
|
"source": {"root": "local", "parts": ["value"]},
|
||||||
|
"target": {"root": "state", "parts": ["result"]},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"routes": {"constant": {"ok": "__end__"}},
|
||||||
|
"output": [
|
||||||
{
|
{
|
||||||
"workspace_id": "remote_ws",
|
"path": {"root": "state", "parts": ["result"]},
|
||||||
"revision": created["result"]["revision"],
|
"target": {"root": "local", "parts": ["result"]},
|
||||||
"patch": [
|
}
|
||||||
{"op": "replace", "path": "/name", "value": "remote_renamed"}
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_draft = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.drafts.validate",
|
||||||
|
{"draft": draft},
|
||||||
|
)
|
||||||
|
compiled_plan = validate_draft["result"]["compiled_plan"]
|
||||||
|
artifact = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.artifacts.save",
|
||||||
|
{
|
||||||
|
"artifact": {
|
||||||
|
"id": "constant_rpc",
|
||||||
|
"version": 1,
|
||||||
|
"kind": "wrapper",
|
||||||
|
"title": "Constant RPC",
|
||||||
|
"input_schema": {"type": "object", "properties": {}},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"result": {"type": "string"}},
|
||||||
|
"required": ["result"],
|
||||||
|
},
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
"required_capabilities": {},
|
||||||
|
"source_bindings": {"wf.std": "wf.std"},
|
||||||
|
"plan": compiled_plan,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
deployment = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.deployments.save",
|
||||||
|
{
|
||||||
|
"deployment": {
|
||||||
|
"id": "constant_rpc.default",
|
||||||
|
"artifact_id": "constant_rpc",
|
||||||
|
"artifact_version": 1,
|
||||||
|
"bindings": [
|
||||||
|
{"logical_source": "wf.std", "concrete_source": "wf.std"}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
},
|
||||||
artifact = await _rpc(
|
)
|
||||||
client,
|
validate_deployment = await _rpc(
|
||||||
"workflow.draft_workspaces.create_artifact",
|
client,
|
||||||
{
|
"workflow.deployments.validate",
|
||||||
"workspace_id": "remote_ws",
|
{"deployment_id": "constant_rpc.default"},
|
||||||
"artifact_id": "remote_artifact",
|
)
|
||||||
"version": 1,
|
|
||||||
"title": "Remote Artifact",
|
|
||||||
"outcomes": ["ok"],
|
|
||||||
"kind": "workflow",
|
|
||||||
"source_bindings": {"wf.std": "wf.std"},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert created["result"]["workspace_id"] == "remote_ws"
|
assert draft_ws["result"]["workspace_id"] == "constant_ws"
|
||||||
assert listed["result"]["workspaces"]
|
assert validate_draft["result"]["status"] == "valid"
|
||||||
assert fetched["result"]["workspace_id"] == "remote_ws"
|
assert artifact["result"]["artifact_id"] == "constant_rpc"
|
||||||
assert validated["result"]["status"] in {"valid", "invalid"}
|
assert deployment["result"]["deployment_id"] == "constant_rpc.default"
|
||||||
assert patched["result"]["revision"] == created["result"]["revision"] + 1
|
assert validate_deployment["result"]["status"] == "runnable"
|
||||||
assert artifact["result"]["artifact_id"] == "remote_artifact"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
async def test_rpc_artifact_and_deployment_catalog_methods(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
await server.api.create_artifact_from_plan(
|
||||||
|
artifact_id="rpc_lifecycle",
|
||||||
|
version=1,
|
||||||
|
title="RPC Lifecycle",
|
||||||
|
plan=_constant_plan(),
|
||||||
|
outcomes=["ok"],
|
||||||
|
source_bindings={"wf.std": "wf.std"},
|
||||||
|
)
|
||||||
|
await server.api.save_deployment(
|
||||||
|
{
|
||||||
|
"id": "rpc_lifecycle.default",
|
||||||
|
"artifact_id": "rpc_lifecycle",
|
||||||
|
"artifact_version": 1,
|
||||||
|
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
listed_artifacts = await _rpc(client, "workflow.artifacts.list", {})
|
||||||
|
inspected_artifact = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.artifacts.inspect",
|
||||||
|
{"artifact_id": "rpc_lifecycle", "version": 1},
|
||||||
|
)
|
||||||
|
listed_deployments = await _rpc(client, "workflow.deployments.list", {})
|
||||||
|
inspected_deployment = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.deployments.inspect",
|
||||||
|
{"deployment_id": "rpc_lifecycle.default"},
|
||||||
|
)
|
||||||
|
deleted = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.deployments.delete",
|
||||||
|
{"deployment_id": "rpc_lifecycle.default"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert listed_artifacts["result"]["nodes"]
|
||||||
|
assert inspected_artifact["result"]["id"] == "rpc_lifecycle"
|
||||||
|
assert listed_deployments["result"]["deployments"]
|
||||||
|
assert inspected_deployment["result"]["id"] == "rpc_lifecycle.default"
|
||||||
|
assert deleted["result"]["deployment_id"] == "rpc_lifecycle.default"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_draft_workspace_methods(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:
|
||||||
|
created = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.create_from_capability",
|
||||||
|
{
|
||||||
|
"workspace_id": "remote_ws",
|
||||||
|
"capability_name": "wf.std.constant",
|
||||||
|
"name": "remote_constant",
|
||||||
|
"title": "Remote Constant",
|
||||||
|
"input_map": {},
|
||||||
|
"output_map": {"value": "state.result"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
listed = await _rpc(client, "workflow.draft_workspaces.list", {})
|
||||||
|
fetched = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.get",
|
||||||
|
{"workspace_id": "remote_ws"},
|
||||||
|
)
|
||||||
|
validated = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.validate",
|
||||||
|
{"workspace_id": "remote_ws"},
|
||||||
|
)
|
||||||
|
patched = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.patch",
|
||||||
|
{
|
||||||
|
"workspace_id": "remote_ws",
|
||||||
|
"revision": created["result"]["revision"],
|
||||||
|
"patch": [
|
||||||
|
{"op": "replace", "path": "/name", "value": "remote_renamed"}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
artifact = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.draft_workspaces.create_artifact",
|
||||||
|
{
|
||||||
|
"workspace_id": "remote_ws",
|
||||||
|
"artifact_id": "remote_artifact",
|
||||||
|
"version": 1,
|
||||||
|
"title": "Remote Artifact",
|
||||||
|
"outcomes": ["ok"],
|
||||||
|
"kind": "workflow",
|
||||||
|
"source_bindings": {"wf.std": "wf.std"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert created["result"]["workspace_id"] == "remote_ws"
|
||||||
|
assert listed["result"]["workspaces"]
|
||||||
|
assert fetched["result"]["workspace_id"] == "remote_ws"
|
||||||
|
assert validated["result"]["status"] in {"valid", "invalid"}
|
||||||
|
assert patched["result"]["revision"] == created["result"]["revision"] + 1
|
||||||
|
assert artifact["result"]["artifact_id"] == "remote_artifact"
|
||||||
|
|
||||||
|
|
||||||
def _constant_plan() -> RawWorkflowPlan:
|
def _constant_plan() -> RawWorkflowPlan:
|
||||||
@@ -377,60 +358,57 @@ def _constant_plan() -> RawWorkflowPlan:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_runs_deployment_and_reads_bounded_trace(tmp_path) -> None:
|
async def test_rpc_runs_deployment_and_reads_bounded_trace(tmp_path) -> None:
|
||||||
async def scenario() -> 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(
|
artifact_id="rpc_constant",
|
||||||
artifact_id="rpc_constant",
|
version=1,
|
||||||
version=1,
|
title="RPC Constant",
|
||||||
title="RPC Constant",
|
plan=_constant_plan(),
|
||||||
plan=_constant_plan(),
|
outcomes=["ok"],
|
||||||
outcomes=["ok"],
|
source_bindings={"wf.std": "wf.std"},
|
||||||
source_bindings={"wf.std": "wf.std"},
|
)
|
||||||
)
|
await server.api.save_deployment(
|
||||||
await server.api.save_deployment(
|
{
|
||||||
|
"id": "rpc_constant.default",
|
||||||
|
"artifact_id": "rpc_constant",
|
||||||
|
"artifact_version": 1,
|
||||||
|
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
run = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.runs.start",
|
||||||
{
|
{
|
||||||
"id": "rpc_constant.default",
|
"deployment_id": "rpc_constant.default",
|
||||||
"artifact_id": "rpc_constant",
|
"workflow_input": {},
|
||||||
"artifact_version": 1,
|
"trace_range": {"start": 0, "limit": 1},
|
||||||
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
},
|
||||||
}
|
)
|
||||||
|
inspected = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.runs.inspect",
|
||||||
|
{"run_id": run["result"]["run_id"]},
|
||||||
|
)
|
||||||
|
trace = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.runs.trace",
|
||||||
|
{
|
||||||
|
"run_id": run["result"]["run_id"],
|
||||||
|
"trace_range": {"start": 0, "limit": 1},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
app = create_rpc_app(server)
|
assert run["result"]["status"] == "completed"
|
||||||
transport = httpx.ASGITransport(app=app)
|
assert run["result"]["output"]["result"] == "hello over rpc"
|
||||||
async with httpx.AsyncClient(
|
assert "trace" not in inspected["result"]
|
||||||
transport=transport, base_url="http://test"
|
assert inspected["result"]["trace_count"] >= 1
|
||||||
) as client:
|
assert trace["result"]["trace_start"] == 0
|
||||||
run = await _rpc(
|
assert trace["result"]["trace_limit"] == 1
|
||||||
client,
|
assert len(trace["result"]["trace"]) == 1
|
||||||
"workflow.runs.start",
|
|
||||||
{
|
|
||||||
"deployment_id": "rpc_constant.default",
|
|
||||||
"workflow_input": {},
|
|
||||||
"trace_range": {"start": 0, "limit": 1},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
inspected = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.runs.inspect",
|
|
||||||
{"run_id": run["result"]["run_id"]},
|
|
||||||
)
|
|
||||||
trace = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.runs.trace",
|
|
||||||
{
|
|
||||||
"run_id": run["result"]["run_id"],
|
|
||||||
"trace_range": {"start": 0, "limit": 1},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert run["result"]["status"] == "completed"
|
|
||||||
assert run["result"]["output"]["result"] == "hello over rpc"
|
|
||||||
assert "trace" not in inspected["result"]
|
|
||||||
assert inspected["result"]["trace_count"] >= 1
|
|
||||||
assert trace["result"]["trace_start"] == 0
|
|
||||||
assert trace["result"]["trace_limit"] == 1
|
|
||||||
assert len(trace["result"]["trace"]) == 1
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from wf_api.models import RawWorkflowPlan, TraceRange
|
from wf_api.models import RawWorkflowPlan, TraceRange
|
||||||
@@ -58,282 +56,258 @@ def _constant_plan() -> RawWorkflowPlan:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_lists_and_inspects_capabilities(tmp_path) -> None:
|
async def test_rpc_workflow_client_lists_and_inspects_capabilities(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport,
|
||||||
transport=transport,
|
base_url="http://test",
|
||||||
base_url="http://test",
|
) as http_client:
|
||||||
) as http_client:
|
client = RpcWorkflowApiClient(
|
||||||
client = RpcWorkflowApiClient(
|
url="http://test/rpc",
|
||||||
url="http://test/rpc",
|
timeout_seconds=5,
|
||||||
timeout_seconds=5,
|
http_client=http_client,
|
||||||
http_client=http_client,
|
|
||||||
)
|
|
||||||
listed = await client.list_capabilities(source_id="wf.std", limit=5)
|
|
||||||
inspected = await client.inspect_capability(
|
|
||||||
qualified_name="wf.std.constant"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert listed["capabilities"]
|
|
||||||
assert inspected["name"] == "wf.std.constant"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_lists_and_inspects_sources(tmp_path) -> None:
|
|
||||||
async def scenario() -> 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,
|
|
||||||
)
|
|
||||||
listed = await client.list_sources(limit=10)
|
|
||||||
inspected = await client.inspect_source(source_id="wf.std")
|
|
||||||
|
|
||||||
source_ids = {source["id"] for source in listed["sources"]}
|
|
||||||
assert "wf.std" in source_ids
|
|
||||||
assert inspected["id"] == "wf.std"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_reads_admin_state(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
|
||||||
server.events.record_workflow_event(
|
|
||||||
"workflow_test_event",
|
|
||||||
capability_id="workflow.demo.v1",
|
|
||||||
payload={"ok": True},
|
|
||||||
)
|
)
|
||||||
app = create_rpc_app(server)
|
listed = await client.list_capabilities(source_id="wf.std", limit=5)
|
||||||
transport = httpx.ASGITransport(app=app)
|
inspected = await client.inspect_capability(
|
||||||
async with httpx.AsyncClient(
|
qualified_name="wf.std.constant"
|
||||||
transport=transport,
|
|
||||||
base_url="http://test",
|
|
||||||
) as http_client:
|
|
||||||
client = RpcWorkflowApiClient(
|
|
||||||
url="http://test/rpc",
|
|
||||||
timeout_seconds=5,
|
|
||||||
http_client=http_client,
|
|
||||||
)
|
|
||||||
connections = await client.list_connections()
|
|
||||||
statuses = await client.get_connection_statuses()
|
|
||||||
events = await client.list_events()
|
|
||||||
|
|
||||||
assert connections == {"connections": [], "total": 0}
|
|
||||||
assert statuses == {"statuses": [], "total": 0}
|
|
||||||
assert events["total"] == 1
|
|
||||||
assert events["events"][0]["kind"] == "workflow_test_event"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_runs_and_reads_trace(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
|
||||||
await server.api.create_artifact_from_plan(
|
|
||||||
artifact_id="client_constant",
|
|
||||||
version=1,
|
|
||||||
title="Client Constant",
|
|
||||||
plan=_constant_plan(),
|
|
||||||
outcomes=["ok"],
|
|
||||||
source_bindings={"wf.std": "wf.std"},
|
|
||||||
)
|
)
|
||||||
await server.api.save_deployment(
|
|
||||||
{
|
assert listed["capabilities"]
|
||||||
"id": "client_constant.default",
|
assert inspected["name"] == "wf.std.constant"
|
||||||
"artifact_id": "client_constant",
|
|
||||||
"artifact_version": 1,
|
|
||||||
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
async def test_rpc_workflow_client_lists_and_inspects_sources(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,
|
||||||
)
|
)
|
||||||
app = create_rpc_app(server)
|
listed = await client.list_sources(limit=10)
|
||||||
transport = httpx.ASGITransport(app=app)
|
inspected = await client.inspect_source(source_id="wf.std")
|
||||||
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,
|
|
||||||
)
|
|
||||||
run = await client.run_deployment(
|
|
||||||
deployment_id="client_constant.default",
|
|
||||||
workflow_input={},
|
|
||||||
trace_range=TraceRange(start=0, limit=1),
|
|
||||||
)
|
|
||||||
inspected = await client.inspect_run(run_id=run["run_id"])
|
|
||||||
trace = await client.read_run_trace(
|
|
||||||
run_id=run["run_id"],
|
|
||||||
trace_range=TraceRange(start=0, limit=1),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert run["status"] == "completed"
|
source_ids = {source["id"] for source in listed["sources"]}
|
||||||
assert run["output"]["result"] == "hello from rpc client"
|
assert "wf.std" in source_ids
|
||||||
assert inspected["trace_count"] >= 1
|
assert inspected["id"] == "wf.std"
|
||||||
assert len(trace["trace"]) == 1
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_raises_for_rpc_error(tmp_path) -> None:
|
async def test_rpc_workflow_client_reads_admin_state(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
server.events.record_workflow_event(
|
||||||
app = create_rpc_app(server)
|
"workflow_test_event",
|
||||||
transport = httpx.ASGITransport(app=app)
|
capability_id="workflow.demo.v1",
|
||||||
async with httpx.AsyncClient(
|
payload={"ok": True},
|
||||||
transport=transport,
|
)
|
||||||
base_url="http://test",
|
app = create_rpc_app(server)
|
||||||
) as http_client:
|
transport = httpx.ASGITransport(app=app)
|
||||||
client = RpcWorkflowApiClient(
|
async with httpx.AsyncClient(
|
||||||
url="http://test/rpc",
|
transport=transport,
|
||||||
timeout_seconds=5,
|
base_url="http://test",
|
||||||
http_client=http_client,
|
) as http_client:
|
||||||
)
|
client = RpcWorkflowApiClient(
|
||||||
try:
|
url="http://test/rpc",
|
||||||
await client.inspect_capability(qualified_name="missing.capability")
|
timeout_seconds=5,
|
||||||
except RuntimeError as exc:
|
http_client=http_client,
|
||||||
message = str(exc)
|
|
||||||
else:
|
|
||||||
raise AssertionError("expected RuntimeError")
|
|
||||||
|
|
||||||
assert "Workflow operation failed" in message
|
|
||||||
assert "missing.capability" in message
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_lists_and_inspects_artifacts(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
|
||||||
await server.api.create_artifact_from_plan(
|
|
||||||
artifact_id="client_art",
|
|
||||||
version=1,
|
|
||||||
title="Client Art",
|
|
||||||
plan=_constant_plan(),
|
|
||||||
outcomes=["ok"],
|
|
||||||
source_bindings={"wf.std": "wf.std"},
|
|
||||||
)
|
)
|
||||||
app = create_rpc_app(server)
|
connections = await client.list_connections()
|
||||||
transport = httpx.ASGITransport(app=app)
|
statuses = await client.get_connection_statuses()
|
||||||
async with httpx.AsyncClient(
|
events = await client.list_events()
|
||||||
transport=transport, base_url="http://test"
|
|
||||||
) as http_client:
|
|
||||||
client = RpcWorkflowApiClient(
|
|
||||||
url="http://test/rpc", timeout_seconds=5, http_client=http_client
|
|
||||||
)
|
|
||||||
listed = await client.list_artifacts()
|
|
||||||
inspected = await client.inspect_artifact(
|
|
||||||
artifact_id="client_art", version=1
|
|
||||||
)
|
|
||||||
|
|
||||||
assert listed["nodes"]
|
assert connections == {"connections": [], "total": 0}
|
||||||
assert inspected["id"] == "client_art"
|
assert statuses == {"statuses": [], "total": 0}
|
||||||
|
assert events["total"] == 1
|
||||||
asyncio.run(scenario())
|
assert events["events"][0]["kind"] == "workflow_test_event"
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_lists_inspects_validates_and_deletes_deployments(
|
async def test_rpc_workflow_client_runs_and_reads_trace(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
await server.api.create_artifact_from_plan(
|
||||||
|
artifact_id="client_constant",
|
||||||
|
version=1,
|
||||||
|
title="Client Constant",
|
||||||
|
plan=_constant_plan(),
|
||||||
|
outcomes=["ok"],
|
||||||
|
source_bindings={"wf.std": "wf.std"},
|
||||||
|
)
|
||||||
|
await server.api.save_deployment(
|
||||||
|
{
|
||||||
|
"id": "client_constant.default",
|
||||||
|
"artifact_id": "client_constant",
|
||||||
|
"artifact_version": 1,
|
||||||
|
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
run = await client.run_deployment(
|
||||||
|
deployment_id="client_constant.default",
|
||||||
|
workflow_input={},
|
||||||
|
trace_range=TraceRange(start=0, limit=1),
|
||||||
|
)
|
||||||
|
inspected = await client.inspect_run(run_id=run["run_id"])
|
||||||
|
trace = await client.read_run_trace(
|
||||||
|
run_id=run["run_id"],
|
||||||
|
trace_range=TraceRange(start=0, limit=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert run["status"] == "completed"
|
||||||
|
assert run["output"]["result"] == "hello from rpc client"
|
||||||
|
assert inspected["trace_count"] >= 1
|
||||||
|
assert len(trace["trace"]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_workflow_client_raises_for_rpc_error(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,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await client.inspect_capability(qualified_name="missing.capability")
|
||||||
|
except RuntimeError as exc:
|
||||||
|
message = str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("expected RuntimeError")
|
||||||
|
|
||||||
|
assert "Workflow operation failed" in message
|
||||||
|
assert "missing.capability" in message
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_workflow_client_lists_and_inspects_artifacts(tmp_path) -> None:
|
||||||
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
|
await server.api.create_artifact_from_plan(
|
||||||
|
artifact_id="client_art",
|
||||||
|
version=1,
|
||||||
|
title="Client Art",
|
||||||
|
plan=_constant_plan(),
|
||||||
|
outcomes=["ok"],
|
||||||
|
source_bindings={"wf.std": "wf.std"},
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
listed = await client.list_artifacts()
|
||||||
|
inspected = await client.inspect_artifact(
|
||||||
|
artifact_id="client_art", version=1
|
||||||
|
)
|
||||||
|
|
||||||
|
assert listed["nodes"]
|
||||||
|
assert inspected["id"] == "client_art"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_workflow_client_lists_inspects_validates_and_deletes_deployments(
|
||||||
tmp_path,
|
tmp_path,
|
||||||
) -> None:
|
) -> None:
|
||||||
async def scenario() -> 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(
|
artifact_id="client_deploy_art",
|
||||||
artifact_id="client_deploy_art",
|
version=1,
|
||||||
|
title="Client Deploy Art",
|
||||||
|
plan=_constant_plan(),
|
||||||
|
outcomes=["ok"],
|
||||||
|
source_bindings={"wf.std": "wf.std"},
|
||||||
|
)
|
||||||
|
await server.api.save_deployment(
|
||||||
|
{
|
||||||
|
"id": "client_deploy_art.default",
|
||||||
|
"artifact_id": "client_deploy_art",
|
||||||
|
"artifact_version": 1,
|
||||||
|
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
listed = await client.list_deployments()
|
||||||
|
inspected = await client.inspect_deployment(
|
||||||
|
deployment_id="client_deploy_art.default"
|
||||||
|
)
|
||||||
|
validated = await client.validate_deployment(
|
||||||
|
deployment_id="client_deploy_art.default"
|
||||||
|
)
|
||||||
|
deleted = await client.delete_deployment(
|
||||||
|
deployment_id="client_deploy_art.default"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert listed["deployments"]
|
||||||
|
assert inspected["id"] == "client_deploy_art.default"
|
||||||
|
assert validated["status"] == "runnable"
|
||||||
|
assert deleted["deployment_id"] == "client_deploy_art.default"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_workflow_client_draft_workspace_lifecycle(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
|
||||||
|
)
|
||||||
|
created = await client.create_draft_workspace_from_capability(
|
||||||
|
workspace_id="client_ws",
|
||||||
|
capability_name="wf.std.constant",
|
||||||
|
name="client_constant",
|
||||||
|
title="Client Constant",
|
||||||
|
input_map={},
|
||||||
|
output_map={"value": "state.result"},
|
||||||
|
)
|
||||||
|
listed = await client.list_draft_workspaces()
|
||||||
|
fetched = await client.get_draft_workspace(workspace_id="client_ws")
|
||||||
|
validated = await client.validate_draft_workspace(workspace_id="client_ws")
|
||||||
|
patched = await client.patch_draft_workspace(
|
||||||
|
workspace_id="client_ws",
|
||||||
|
revision=created["revision"],
|
||||||
|
patch=[{"op": "replace", "path": "/name", "value": "client_renamed"}],
|
||||||
|
)
|
||||||
|
artifact = await client.create_artifact_from_workspace(
|
||||||
|
workspace_id="client_ws",
|
||||||
|
artifact_id="client_ws_art",
|
||||||
version=1,
|
version=1,
|
||||||
title="Client Deploy Art",
|
title="Client WS Art",
|
||||||
plan=_constant_plan(),
|
outcomes=("ok",),
|
||||||
outcomes=["ok"],
|
kind="workflow",
|
||||||
source_bindings={"wf.std": "wf.std"},
|
source_bindings={"wf.std": "wf.std"},
|
||||||
)
|
)
|
||||||
await server.api.save_deployment(
|
|
||||||
{
|
|
||||||
"id": "client_deploy_art.default",
|
|
||||||
"artifact_id": "client_deploy_art",
|
|
||||||
"artifact_version": 1,
|
|
||||||
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
listed = await client.list_deployments()
|
|
||||||
inspected = await client.inspect_deployment(
|
|
||||||
deployment_id="client_deploy_art.default"
|
|
||||||
)
|
|
||||||
validated = await client.validate_deployment(
|
|
||||||
deployment_id="client_deploy_art.default"
|
|
||||||
)
|
|
||||||
deleted = await client.delete_deployment(
|
|
||||||
deployment_id="client_deploy_art.default"
|
|
||||||
)
|
|
||||||
|
|
||||||
assert listed["deployments"]
|
assert created["workspace_id"] == "client_ws"
|
||||||
assert inspected["id"] == "client_deploy_art.default"
|
assert listed["workspaces"]
|
||||||
assert validated["status"] == "runnable"
|
assert fetched["workspace_id"] == "client_ws"
|
||||||
assert deleted["deployment_id"] == "client_deploy_art.default"
|
assert validated["status"] in {"valid", "invalid"}
|
||||||
|
assert patched["revision"] == created["revision"] + 1
|
||||||
asyncio.run(scenario())
|
assert artifact["artifact_id"] == "client_ws_art"
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_workflow_client_draft_workspace_lifecycle(tmp_path) -> None:
|
|
||||||
async def scenario() -> 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
|
|
||||||
)
|
|
||||||
created = await client.create_draft_workspace_from_capability(
|
|
||||||
workspace_id="client_ws",
|
|
||||||
capability_name="wf.std.constant",
|
|
||||||
name="client_constant",
|
|
||||||
title="Client Constant",
|
|
||||||
input_map={},
|
|
||||||
output_map={"value": "state.result"},
|
|
||||||
)
|
|
||||||
listed = await client.list_draft_workspaces()
|
|
||||||
fetched = await client.get_draft_workspace(workspace_id="client_ws")
|
|
||||||
validated = await client.validate_draft_workspace(workspace_id="client_ws")
|
|
||||||
patched = await client.patch_draft_workspace(
|
|
||||||
workspace_id="client_ws",
|
|
||||||
revision=created["revision"],
|
|
||||||
patch=[{"op": "replace", "path": "/name", "value": "client_renamed"}],
|
|
||||||
)
|
|
||||||
artifact = await client.create_artifact_from_workspace(
|
|
||||||
workspace_id="client_ws",
|
|
||||||
artifact_id="client_ws_art",
|
|
||||||
version=1,
|
|
||||||
title="Client WS Art",
|
|
||||||
outcomes=("ok",),
|
|
||||||
kind="workflow",
|
|
||||||
source_bindings={"wf.std": "wf.std"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert created["workspace_id"] == "client_ws"
|
|
||||||
assert listed["workspaces"]
|
|
||||||
assert fetched["workspace_id"] == "client_ws"
|
|
||||||
assert validated["status"] in {"valid", "invalid"}
|
|
||||||
assert patched["revision"] == created["revision"] + 1
|
|
||||||
assert artifact["artifact_id"] == "client_ws_art"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from dataclasses import dataclass, replace
|
from dataclasses import dataclass, replace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -100,390 +99,339 @@ def _server_with_mutation_provider(tmp_path: Any) -> Any:
|
|||||||
# --- read-only tests (unchanged) ---
|
# --- read-only tests (unchanged) ---
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_list_unavailable_on_local_static(tmp_path) -> None:
|
async def test_rpc_source_registry_list_unavailable_on_local_static(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client, "workflow.admin.source_registry.list", {"limit": 10}
|
||||||
client, "workflow.admin.source_registry.list", {"limit": 10}
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
|
||||||
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_inspect_unavailable_on_local_static(tmp_path) -> None:
|
|
||||||
async def scenario() -> 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:
|
|
||||||
payload = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.admin.source_registry.inspect",
|
|
||||||
{"source_id": "github.work"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
|
||||||
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_methods_return_registry_payloads(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
server = replace(
|
|
||||||
build_local_static_workflow_server(tmp_path / "store"),
|
|
||||||
source_registry_admin=WorkflowSourceRegistryApi(
|
|
||||||
provider=FakeRegistryProvider()
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
app = create_rpc_app(server)
|
|
||||||
transport = httpx.ASGITransport(app=app)
|
|
||||||
async with httpx.AsyncClient(
|
|
||||||
transport=transport, base_url="http://test"
|
|
||||||
) as client:
|
|
||||||
listed = await _rpc(
|
|
||||||
client, "workflow.admin.source_registry.list", {"limit": 10}
|
|
||||||
)
|
|
||||||
inspected = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.admin.source_registry.inspect",
|
|
||||||
{"source_id": "github.work"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert listed["result"]["entries"][0]["id"] == "github.work"
|
assert "error" in payload
|
||||||
assert listed["result"]["entries"][0]["shadowed_by_config"] is True
|
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
||||||
assert inspected["result"]["entry"]["transport"]["kind"] == "stdio"
|
|
||||||
assert inspected["result"]["shadowed_by_config"] is True
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
async def test_rpc_source_registry_inspect_unavailable_on_local_static(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:
|
||||||
|
payload = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.admin.source_registry.inspect",
|
||||||
|
{"source_id": "github.work"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "error" in payload
|
||||||
|
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_source_registry_methods_return_registry_payloads(tmp_path) -> None:
|
||||||
|
server = replace(
|
||||||
|
build_local_static_workflow_server(tmp_path / "store"),
|
||||||
|
source_registry_admin=WorkflowSourceRegistryApi(
|
||||||
|
provider=FakeRegistryProvider()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
listed = await _rpc(
|
||||||
|
client, "workflow.admin.source_registry.list", {"limit": 10}
|
||||||
|
)
|
||||||
|
inspected = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.admin.source_registry.inspect",
|
||||||
|
{"source_id": "github.work"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert listed["result"]["entries"][0]["id"] == "github.work"
|
||||||
|
assert listed["result"]["entries"][0]["shadowed_by_config"] is True
|
||||||
|
assert inspected["result"]["entry"]["transport"]["kind"] == "stdio"
|
||||||
|
assert inspected["result"]["shadowed_by_config"] is True
|
||||||
|
|
||||||
|
|
||||||
# --- mutation unavailable tests ---
|
# --- mutation unavailable tests ---
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_add_unavailable_on_local_static(tmp_path) -> None:
|
async def test_rpc_source_registry_add_unavailable_on_local_static(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.add",
|
||||||
"workflow.admin.source_registry.add",
|
{"entry": {"id": "new.source", "kind": "mcp"}},
|
||||||
{"entry": {"id": "new.source", "kind": "mcp"}},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_update_unavailable_on_local_static(tmp_path) -> None:
|
async def test_rpc_source_registry_update_unavailable_on_local_static(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.update",
|
||||||
"workflow.admin.source_registry.update",
|
{"source_id": "github.work", "patch": {"enabled": False}},
|
||||||
{"source_id": "github.work", "patch": {"enabled": False}},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_enable_unavailable_on_local_static(tmp_path) -> None:
|
async def test_rpc_source_registry_enable_unavailable_on_local_static(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.enable",
|
||||||
"workflow.admin.source_registry.enable",
|
{"source_id": "github.work"},
|
||||||
{"source_id": "github.work"},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_disable_unavailable_on_local_static(tmp_path) -> None:
|
async def test_rpc_source_registry_disable_unavailable_on_local_static(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.disable",
|
||||||
"workflow.admin.source_registry.disable",
|
{"source_id": "github.work"},
|
||||||
{"source_id": "github.work"},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_remove_unavailable_on_local_static(tmp_path) -> None:
|
async def test_rpc_source_registry_remove_unavailable_on_local_static(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.remove",
|
||||||
"workflow.admin.source_registry.remove",
|
{"source_id": "github.work"},
|
||||||
{"source_id": "github.work"},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
assert payload["error"]["data"]["code"] == "source_registry_unavailable"
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
# --- mutation success tests ---
|
# --- mutation success tests ---
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_add_returns_entry(tmp_path) -> None:
|
async def test_rpc_source_registry_add_returns_entry(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = _server_with_mutation_provider(tmp_path)
|
||||||
server = _server_with_mutation_provider(tmp_path)
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.add",
|
||||||
"workflow.admin.source_registry.add",
|
{"entry": {"id": "new.mcp", "kind": "mcp", "enabled": True}},
|
||||||
{"entry": {"id": "new.mcp", "kind": "mcp", "enabled": True}},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "result" in payload
|
|
||||||
assert payload["result"]["entry"]["id"] == "new.mcp"
|
|
||||||
assert payload["result"]["entry"]["kind"] == "mcp"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_update_returns_entry(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
server = _server_with_mutation_provider(tmp_path)
|
|
||||||
app = create_rpc_app(server)
|
|
||||||
transport = httpx.ASGITransport(app=app)
|
|
||||||
async with httpx.AsyncClient(
|
|
||||||
transport=transport, base_url="http://test"
|
|
||||||
) as client:
|
|
||||||
payload = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.admin.source_registry.update",
|
|
||||||
{"source_id": "github.work", "patch": {"enabled": False}},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "result" in payload
|
|
||||||
assert payload["result"]["entry"]["id"] == "github.work"
|
|
||||||
assert payload["result"]["entry"]["enabled"] is False
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_enable_returns_entry(tmp_path) -> None:
|
|
||||||
async def scenario() -> None:
|
|
||||||
mutation = FakeMutationProvider()
|
|
||||||
mutation.entries["github.work"]["enabled"] = False
|
|
||||||
server = replace(
|
|
||||||
build_local_static_workflow_server(tmp_path / "store"),
|
|
||||||
source_registry_admin=WorkflowSourceRegistryApi(
|
|
||||||
provider=FakeRegistryProvider(),
|
|
||||||
mutation_provider=mutation,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
app = create_rpc_app(server)
|
|
||||||
transport = httpx.ASGITransport(app=app)
|
|
||||||
async with httpx.AsyncClient(
|
|
||||||
transport=transport, base_url="http://test"
|
|
||||||
) as client:
|
|
||||||
payload = await _rpc(
|
|
||||||
client,
|
|
||||||
"workflow.admin.source_registry.enable",
|
|
||||||
{"source_id": "github.work"},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
assert payload["result"]["entry"]["enabled"] is True
|
assert payload["result"]["entry"]["id"] == "new.mcp"
|
||||||
|
assert payload["result"]["entry"]["kind"] == "mcp"
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_disable_returns_entry(tmp_path) -> None:
|
async def test_rpc_source_registry_update_returns_entry(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = _server_with_mutation_provider(tmp_path)
|
||||||
server = _server_with_mutation_provider(tmp_path)
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.update",
|
||||||
"workflow.admin.source_registry.disable",
|
{"source_id": "github.work", "patch": {"enabled": False}},
|
||||||
{"source_id": "github.work"},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
assert payload["result"]["entry"]["enabled"] is False
|
assert payload["result"]["entry"]["id"] == "github.work"
|
||||||
|
assert payload["result"]["entry"]["enabled"] is False
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_remove_returns_removed(tmp_path) -> None:
|
async def test_rpc_source_registry_enable_returns_entry(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
mutation = FakeMutationProvider()
|
||||||
server = _server_with_mutation_provider(tmp_path)
|
mutation.entries["github.work"]["enabled"] = False
|
||||||
app = create_rpc_app(server)
|
server = replace(
|
||||||
transport = httpx.ASGITransport(app=app)
|
build_local_static_workflow_server(tmp_path / "store"),
|
||||||
async with httpx.AsyncClient(
|
source_registry_admin=WorkflowSourceRegistryApi(
|
||||||
transport=transport, base_url="http://test"
|
provider=FakeRegistryProvider(),
|
||||||
) as client:
|
mutation_provider=mutation,
|
||||||
payload = await _rpc(
|
),
|
||||||
client,
|
)
|
||||||
"workflow.admin.source_registry.remove",
|
app = create_rpc_app(server)
|
||||||
{"source_id": "github.work"},
|
transport = httpx.ASGITransport(app=app)
|
||||||
)
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
payload = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.admin.source_registry.enable",
|
||||||
|
{"source_id": "github.work"},
|
||||||
|
)
|
||||||
|
|
||||||
assert "result" in payload
|
assert "result" in payload
|
||||||
assert payload["result"]["removed"] is True
|
assert payload["result"]["entry"]["enabled"] is True
|
||||||
assert payload["result"]["source_id"] == "github.work"
|
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
async def test_rpc_source_registry_disable_returns_entry(tmp_path) -> None:
|
||||||
|
server = _server_with_mutation_provider(tmp_path)
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
payload = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.admin.source_registry.disable",
|
||||||
|
{"source_id": "github.work"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "result" in payload
|
||||||
|
assert payload["result"]["entry"]["enabled"] is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_source_registry_remove_returns_removed(tmp_path) -> None:
|
||||||
|
server = _server_with_mutation_provider(tmp_path)
|
||||||
|
app = create_rpc_app(server)
|
||||||
|
transport = httpx.ASGITransport(app=app)
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
transport=transport, base_url="http://test"
|
||||||
|
) as client:
|
||||||
|
payload = await _rpc(
|
||||||
|
client,
|
||||||
|
"workflow.admin.source_registry.remove",
|
||||||
|
{"source_id": "github.work"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "result" in payload
|
||||||
|
assert payload["result"]["removed"] is True
|
||||||
|
assert payload["result"]["source_id"] == "github.work"
|
||||||
|
|
||||||
|
|
||||||
# --- mutation error tests ---
|
# --- mutation error tests ---
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_add_missing_entry_raises_error(tmp_path) -> None:
|
async def test_rpc_source_registry_add_missing_entry_raises_error(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = _server_with_mutation_provider(tmp_path)
|
||||||
server = _server_with_mutation_provider(tmp_path)
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.add",
|
||||||
"workflow.admin.source_registry.add",
|
{"entry": {}},
|
||||||
{"entry": {}},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_update_missing_source_raises_error(tmp_path) -> None:
|
async def test_rpc_source_registry_update_missing_source_raises_error(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = _server_with_mutation_provider(tmp_path)
|
||||||
server = _server_with_mutation_provider(tmp_path)
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.update",
|
||||||
"workflow.admin.source_registry.update",
|
{"source_id": "nonexistent", "patch": {"enabled": False}},
|
||||||
{"source_id": "nonexistent", "patch": {"enabled": False}},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_source_registry_remove_missing_source_raises_error(tmp_path) -> None:
|
async def test_rpc_source_registry_remove_missing_source_raises_error(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = _server_with_mutation_provider(tmp_path)
|
||||||
server = _server_with_mutation_provider(tmp_path)
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as client:
|
||||||
) as client:
|
payload = await _rpc(
|
||||||
payload = await _rpc(
|
client,
|
||||||
client,
|
"workflow.admin.source_registry.remove",
|
||||||
"workflow.admin.source_registry.remove",
|
{"source_id": "nonexistent"},
|
||||||
{"source_id": "nonexistent"},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert "error" in payload
|
assert "error" in payload
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
# --- client method tests ---
|
# --- client method tests ---
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_client_source_registry_calls_correct_methods(tmp_path) -> None:
|
async def test_rpc_client_source_registry_calls_correct_methods(tmp_path) -> None:
|
||||||
async def scenario() -> None:
|
server = build_local_static_workflow_server(tmp_path / "store")
|
||||||
server = build_local_static_workflow_server(tmp_path / "store")
|
app = create_rpc_app(server)
|
||||||
app = create_rpc_app(server)
|
transport = httpx.ASGITransport(app=app)
|
||||||
transport = httpx.ASGITransport(app=app)
|
async with httpx.AsyncClient(
|
||||||
async with httpx.AsyncClient(
|
transport=transport, base_url="http://test"
|
||||||
transport=transport, base_url="http://test"
|
) as http_client:
|
||||||
) as http_client:
|
client = RpcWorkflowApiClient(
|
||||||
client = RpcWorkflowApiClient(
|
url="http://test/rpc",
|
||||||
url="http://test/rpc",
|
timeout_seconds=5,
|
||||||
timeout_seconds=5,
|
http_client=http_client,
|
||||||
http_client=http_client,
|
)
|
||||||
)
|
try:
|
||||||
try:
|
await client.list_registry_entries(limit=5)
|
||||||
await client.list_registry_entries(limit=5)
|
except RuntimeError as exc:
|
||||||
except RuntimeError as exc:
|
list_error = str(exc)
|
||||||
list_error = str(exc)
|
else:
|
||||||
else:
|
list_error = None
|
||||||
list_error = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await client.inspect_registry_entry(source_id="x")
|
await client.inspect_registry_entry(source_id="x")
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
inspect_error = str(exc)
|
inspect_error = str(exc)
|
||||||
else:
|
else:
|
||||||
inspect_error = None
|
inspect_error = None
|
||||||
|
|
||||||
assert list_error is not None
|
assert list_error is not None
|
||||||
assert "source registry admin reads are not available" in list_error
|
assert "source registry admin reads are not available" in list_error
|
||||||
assert inspect_error is not None
|
assert inspect_error is not None
|
||||||
assert "source registry admin reads are not available" in inspect_error
|
assert "source registry admin reads are not available" in inspect_error
|
||||||
|
|
||||||
asyncio.run(scenario())
|
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_client_source_registry_mutation_methods_exist() -> None:
|
async def test_rpc_client_source_registry_mutation_methods_exist() -> None:
|
||||||
from wf_transport_rpc_http.client import RpcWorkflowApiClient
|
from wf_transport_rpc_http.client import RpcWorkflowApiClient
|
||||||
|
|
||||||
client = RpcWorkflowApiClient.__new__(RpcWorkflowApiClient)
|
client = RpcWorkflowApiClient.__new__(RpcWorkflowApiClient)
|
||||||
@@ -495,31 +443,31 @@ def test_rpc_client_source_registry_mutation_methods_exist() -> None:
|
|||||||
|
|
||||||
client._call = fake_call # type: ignore[assignment]
|
client._call = fake_call # type: ignore[assignment]
|
||||||
|
|
||||||
asyncio.run(client.add_registry_entry(entry={"id": "x", "kind": "mcp"}))
|
await client.add_registry_entry(entry={"id": "x", "kind": "mcp"})
|
||||||
assert calls[-1] == (
|
assert calls[-1] == (
|
||||||
"workflow.admin.source_registry.add",
|
"workflow.admin.source_registry.add",
|
||||||
{"entry": {"id": "x", "kind": "mcp"}},
|
{"entry": {"id": "x", "kind": "mcp"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run(client.update_registry_entry(source_id="s", patch={"enabled": False}))
|
await client.update_registry_entry(source_id="s", patch={"enabled": False})
|
||||||
assert calls[-1] == (
|
assert calls[-1] == (
|
||||||
"workflow.admin.source_registry.update",
|
"workflow.admin.source_registry.update",
|
||||||
{"source_id": "s", "patch": {"enabled": False}},
|
{"source_id": "s", "patch": {"enabled": False}},
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run(client.enable_registry_entry(source_id="s"))
|
await client.enable_registry_entry(source_id="s")
|
||||||
assert calls[-1] == (
|
assert calls[-1] == (
|
||||||
"workflow.admin.source_registry.enable",
|
"workflow.admin.source_registry.enable",
|
||||||
{"source_id": "s"},
|
{"source_id": "s"},
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run(client.disable_registry_entry(source_id="s"))
|
await client.disable_registry_entry(source_id="s")
|
||||||
assert calls[-1] == (
|
assert calls[-1] == (
|
||||||
"workflow.admin.source_registry.disable",
|
"workflow.admin.source_registry.disable",
|
||||||
{"source_id": "s"},
|
{"source_id": "s"},
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.run(client.remove_registry_entry(source_id="s"))
|
await client.remove_registry_entry(source_id="s")
|
||||||
assert calls[-1] == (
|
assert calls[-1] == (
|
||||||
"workflow.admin.source_registry.remove",
|
"workflow.admin.source_registry.remove",
|
||||||
{"source_id": "s"},
|
{"source_id": "s"},
|
||||||
|
|||||||
@@ -660,6 +660,7 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "basedpyright" },
|
{ name = "basedpyright" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "ruff" },
|
{ name = "ruff" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -681,6 +682,7 @@ requires-dist = [
|
|||||||
dev = [
|
dev = [
|
||||||
{ name = "basedpyright", specifier = ">=1.39.6" },
|
{ name = "basedpyright", specifier = ">=1.39.6" },
|
||||||
{ name = "pytest", specifier = ">=8" },
|
{ name = "pytest", specifier = ">=8" },
|
||||||
|
{ name = "pytest-asyncio", specifier = ">=1.4.0" },
|
||||||
{ name = "ruff", specifier = ">=0.15.15" },
|
{ name = "ruff", specifier = ">=0.15.15" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1065,6 +1067,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user