feat: expose read-only admin surface
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from wf_api import WorkflowAdminApi, WorkflowAdminSurface
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FakeConnection:
|
||||
id: str
|
||||
server: str
|
||||
account: str
|
||||
enabled: bool = True
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FakeEvent:
|
||||
kind: str
|
||||
timestamp_epoch_ms: int
|
||||
connection_id: str | None = None
|
||||
payload: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class FakeAdminProvider:
|
||||
def __init__(self) -> None:
|
||||
self.connections = [
|
||||
FakeConnection(id="zeta.personal", server="zeta", account="personal"),
|
||||
FakeConnection(id="alpha.work", server="alpha", account="work"),
|
||||
]
|
||||
self.statuses = [
|
||||
{"connection_id": "zeta.personal", "enabled": True},
|
||||
{"connection_id": "alpha.work", "enabled": False},
|
||||
]
|
||||
self.events = [
|
||||
FakeEvent(
|
||||
kind="connection_registered",
|
||||
timestamp_epoch_ms=123,
|
||||
connection_id="alpha.work",
|
||||
)
|
||||
]
|
||||
|
||||
def list_connections(self) -> list[FakeConnection]:
|
||||
return self.connections
|
||||
|
||||
def get_connection_statuses(self) -> list[dict[str, Any]]:
|
||||
return self.statuses
|
||||
|
||||
def list_events(self) -> list[FakeEvent]:
|
||||
return self.events
|
||||
|
||||
|
||||
def test_admin_api_lists_connections_in_id_order() -> None:
|
||||
provider = FakeAdminProvider()
|
||||
api = WorkflowAdminApi(connections=provider, events=provider)
|
||||
|
||||
payload = asyncio.run(api.list_connections())
|
||||
|
||||
assert payload["total"] == 2
|
||||
assert [connection["id"] for connection in payload["connections"]] == [
|
||||
"alpha.work",
|
||||
"zeta.personal",
|
||||
]
|
||||
|
||||
|
||||
def test_admin_api_lists_connection_statuses_in_id_order() -> None:
|
||||
provider = FakeAdminProvider()
|
||||
api = WorkflowAdminApi(connections=provider, events=provider)
|
||||
|
||||
payload = asyncio.run(api.get_connection_statuses())
|
||||
|
||||
assert payload["total"] == 2
|
||||
assert [status["connection_id"] for status in payload["statuses"]] == [
|
||||
"alpha.work",
|
||||
"zeta.personal",
|
||||
]
|
||||
|
||||
|
||||
def test_admin_api_lists_events() -> None:
|
||||
provider = FakeAdminProvider()
|
||||
api = WorkflowAdminApi(connections=provider, events=provider)
|
||||
|
||||
payload = asyncio.run(api.list_events())
|
||||
|
||||
assert payload["total"] == 1
|
||||
assert payload["events"][0]["kind"] == "connection_registered"
|
||||
assert payload["events"][0]["connection_id"] == "alpha.work"
|
||||
|
||||
|
||||
def test_admin_api_satisfies_surface_protocol() -> None:
|
||||
provider = FakeAdminProvider()
|
||||
api: WorkflowAdminSurface = WorkflowAdminApi(connections=provider, events=provider)
|
||||
|
||||
assert api is not None
|
||||
@@ -18,6 +18,7 @@ def test_wf_help_lists_lifecycle_groups() -> None:
|
||||
assert "deploy" in result.output
|
||||
assert "run" in result.output
|
||||
assert "source" in result.output
|
||||
assert "admin" in result.output
|
||||
assert "docs" in result.output
|
||||
assert "schema" in result.output
|
||||
assert "explain" in result.output
|
||||
@@ -76,6 +77,13 @@ def test_wf_source_list_help_exists() -> None:
|
||||
assert "--limit" in result.output
|
||||
|
||||
|
||||
def test_wf_admin_connections_help_exists() -> None:
|
||||
result = runner.invoke(app, ["admin", "connections", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "--format" in result.output
|
||||
|
||||
|
||||
def test_wf_artifact_list_help_exists() -> None:
|
||||
result = runner.invoke(app, ["artifact", "list", "--help"])
|
||||
|
||||
|
||||
@@ -310,6 +310,38 @@ def test_wf_source_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
assert '"id": "wf.std"' in inspected.output
|
||||
|
||||
|
||||
def test_wf_admin_commands_use_rpc_url_override(monkeypatch, tmp_path) -> 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},
|
||||
)
|
||||
original_client = httpx.AsyncClient
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.client.httpx.AsyncClient",
|
||||
lambda *args, **kwargs: original_client(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server)),
|
||||
base_url="http://test",
|
||||
),
|
||||
)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
runner = CliRunner()
|
||||
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
|
||||
|
||||
connections = runner.invoke(app, [*base_args, "admin", "connections"])
|
||||
statuses = runner.invoke(app, [*base_args, "admin", "statuses"])
|
||||
events = runner.invoke(app, [*base_args, "admin", "events"])
|
||||
|
||||
assert connections.exit_code == 0, connections.output
|
||||
assert '"connections": []' in connections.output
|
||||
assert statuses.exit_code == 0, statuses.output
|
||||
assert '"statuses": []' in statuses.output
|
||||
assert events.exit_code == 0, events.output
|
||||
assert '"kind": "workflow_test_event"' in events.output
|
||||
|
||||
|
||||
def test_wf_remote_draft_artifact_deploy_lifecycle(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
original_client = httpx.AsyncClient
|
||||
|
||||
@@ -18,8 +18,8 @@ def test_broker_admin_handlers_list_connections_and_events() -> None:
|
||||
)
|
||||
handlers = BrokerAdminHandlers(service)
|
||||
|
||||
connections = handlers.list_connections()
|
||||
events = handlers.get_broker_events()
|
||||
connections = _run(handlers.list_connections())
|
||||
events = _run(handlers.get_broker_events())
|
||||
|
||||
assert connections[0]["id"] == "demo.personal"
|
||||
assert connections[0]["server"] == "demo"
|
||||
|
||||
@@ -107,6 +107,37 @@ def test_rpc_workflow_client_lists_and_inspects_sources(tmp_path) -> None:
|
||||
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)
|
||||
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,
|
||||
)
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user