test: migrate service async tests

This commit is contained in:
lda
2026-06-04 19:12:12 +07:00 Unverified
parent f9673a4ad0
commit e001b524d6
8 changed files with 163 additions and 213 deletions
+6 -8
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from wf_mcp.broker import WfMcpService from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore from wf_mcp.storage import FileStore
@@ -13,21 +11,21 @@ from ..test_support import (
from .conftest import ContentOnlyOutputAdapter from .conftest import ContentOnlyOutputAdapter
def test_service_catalog_preserves_json_schema_description_metadata() -> None: async def test_service_catalog_preserves_json_schema_description_metadata() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "schema_doc_store")) service = WfMcpService(store=FileStore(local_temp_root() / "schema_doc_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
payload = service.get_catalog().as_payload() payload = service.get_catalog().as_payload()
node = payload["nodes"][0] node = payload["nodes"][0]
assert node["input_schema"]["properties"]["text"]["description"] == "Text to echo" assert node["input_schema"]["properties"]["text"]["description"] == "Text to echo"
def test_service_preserves_content_only_tool_output_schema_for_workflows() -> None: async def test_service_preserves_content_only_tool_output_schema_for_workflows() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "content_only_store")) service = WfMcpService(store=FileStore(local_temp_root() / "content_only_store"))
service.register_connection( service.register_connection(
ConnectionConfig( ConnectionConfig(
@@ -36,7 +34,7 @@ def test_service_preserves_content_only_tool_output_schema_for_workflows() -> No
) )
service.register_adapter("everything", ContentOnlyOutputAdapter()) service.register_adapter("everything", ContentOnlyOutputAdapter())
asyncio.run(service.refresh_connection_catalog("everything.default")) await service.refresh_connection_catalog("everything.default")
payload = service.get_catalog().as_payload() payload = service.get_catalog().as_payload()
node = payload["nodes"][0] node = payload["nodes"][0]
@@ -44,14 +42,14 @@ def test_service_preserves_content_only_tool_output_schema_for_workflows() -> No
assert node["output_schema"]["required"] == ["content"] assert node["output_schema"]["required"] == ["content"]
def test_service_wrapped_tool_adapter_model_validates_simple_schema_types() -> None: async def test_service_wrapped_tool_adapter_model_validates_simple_schema_types() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "adapter_model_store")) service = WfMcpService(store=FileStore(local_temp_root() / "adapter_model_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
source = service.capability_sources["demo.personal"] source = service.capability_sources["demo.personal"]
spec = source.capabilities.node_specs["demo.personal.echo_tool"] spec = source.capabilities.node_specs["demo.personal.echo_tool"]
+7 -10
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import shutil import shutil
from wf_authoring import NodeSpec from wf_authoring import NodeSpec
@@ -268,7 +267,7 @@ def test_service_catalog_split_keeps_system_specs_out_of_backend_catalog() -> No
assert "wf.std.runtime_error" in available_names assert "wf.std.runtime_error" in available_names
def test_service_hydrates_planner_specs_from_stored_catalog() -> None: async def test_service_hydrates_planner_specs_from_stored_catalog() -> None:
store = local_temp_root() / "restart_planner_store" store = local_temp_root() / "restart_planner_store"
shutil.rmtree(store, ignore_errors=True) shutil.rmtree(store, ignore_errors=True)
first_service = WfMcpService(store=FileStore(store)) first_service = WfMcpService(store=FileStore(store))
@@ -276,7 +275,7 @@ def test_service_hydrates_planner_specs_from_stored_catalog() -> None:
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
first_service.register_adapter("demo", FakeAdapter()) first_service.register_adapter("demo", FakeAdapter())
asyncio.run(first_service.refresh_connection_catalog("demo.personal")) await first_service.refresh_connection_catalog("demo.personal")
second_service = WfMcpService(store=FileStore(store)) second_service = WfMcpService(store=FileStore(store))
second_service.register_connection( second_service.register_connection(
@@ -288,11 +287,9 @@ def test_service_hydrates_planner_specs_from_stored_catalog() -> None:
node["qualified_name"] node["qualified_name"]
for node in second_service.get_planner_catalog().as_payload()["nodes"] for node in second_service.get_planner_catalog().as_payload()["nodes"]
} }
run = asyncio.run( run = await second_service.run_workflow_from_plan(
second_service.run_workflow_from_plan( single_echo_plan("hydrated_plan", "demo.personal.echo_tool"),
single_echo_plan("hydrated_plan", "demo.personal.echo_tool"), {"text": "hello"},
{"text": "hello"},
)
) )
assert "demo.personal.echo_tool" in planner_names assert "demo.personal.echo_tool" in planner_names
@@ -412,7 +409,7 @@ def test_source_catalog_service_excludes_hidden_sources_from_planner_catalog() -
assert "hidden.source.echo_tool" not in planner_names assert "hidden.source.echo_tool" not in planner_names
def test_source_catalog_hydrates_connection_source_from_snapshot_directly() -> None: async def test_source_catalog_hydrates_connection_source_from_snapshot_directly() -> None:
root = local_temp_root() / "source_catalog_hydrate_direct" root = local_temp_root() / "source_catalog_hydrate_direct"
shutil.rmtree(root, ignore_errors=True) shutil.rmtree(root, ignore_errors=True)
first_service = WfMcpService(store=FileStore(root)) first_service = WfMcpService(store=FileStore(root))
@@ -420,7 +417,7 @@ def test_source_catalog_hydrates_connection_source_from_snapshot_directly() -> N
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
first_service.register_adapter("demo", FakeAdapter()) first_service.register_adapter("demo", FakeAdapter())
asyncio.run(first_service.refresh_connection_catalog("demo.personal")) await first_service.refresh_connection_catalog("demo.personal")
second_service = WfMcpService(store=FileStore(root)) second_service = WfMcpService(store=FileStore(root))
second_service.register_connection( second_service.register_connection(
+14 -20
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import pytest import pytest
from wf_mcp.broker.service.content_access import ContentAccessService from wf_mcp.broker.service.content_access import ContentAccessService
@@ -87,17 +85,17 @@ def _register_local_docs(source_catalog: SourceCatalogService) -> None:
) )
def test_content_access_reads_local_documentation_resource() -> None: async def test_content_access_reads_local_documentation_resource() -> None:
content_access, events = _make_content_access() content_access, events = _make_content_access()
result = asyncio.run(content_access.read_resource("test.docs.example")) result = await content_access.read_resource("test.docs.example")
assert result["contents"][0]["uri"] == "wf://docs/example" assert result["contents"][0]["uri"] == "wf://docs/example"
assert result["contents"][0]["text"] == "# Example" assert result["contents"][0]["text"] == "# Example"
assert "resource_read_completed" in [e.kind for e in events.list_events()] assert "resource_read_completed" in [e.kind for e in events.list_events()]
def test_content_access_reads_upstream_resource_with_events() -> None: async def test_content_access_reads_upstream_resource_with_events() -> None:
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "content_upstream_resource") store=FileStore(local_temp_root() / "content_upstream_resource")
) )
@@ -105,11 +103,9 @@ def test_content_access_reads_upstream_resource_with_events() -> None:
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
result = asyncio.run( result = await service.content_access.read_resource("demo.personal.resource.welcome")
service.content_access.read_resource("demo.personal.resource.welcome")
)
assert result["contents"][0]["text"] == "Welcome from the fake adapter resource." assert result["contents"][0]["text"] == "Welcome from the fake adapter resource."
event_kinds = [e.kind for e in service.list_events()] event_kinds = [e.kind for e in service.list_events()]
@@ -117,7 +113,7 @@ def test_content_access_reads_upstream_resource_with_events() -> None:
assert "resource_read_completed" in event_kinds assert "resource_read_completed" in event_kinds
def test_content_access_renders_upstream_prompt_with_events() -> None: async def test_content_access_renders_upstream_prompt_with_events() -> None:
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "content_upstream_prompt") store=FileStore(local_temp_root() / "content_upstream_prompt")
) )
@@ -125,13 +121,11 @@ def test_content_access_renders_upstream_prompt_with_events() -> None:
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
result = asyncio.run( result = await service.content_access.render_prompt(
service.content_access.render_prompt( "demo.personal.prompt.summarize",
"demo.personal.prompt.summarize", arguments={"text": "hello world"},
arguments={"text": "hello world"},
)
) )
assert "hello world" in result["messages"][0]["content"]["text"] assert "hello world" in result["messages"][0]["content"]["text"]
@@ -140,12 +134,12 @@ def test_content_access_renders_upstream_prompt_with_events() -> None:
assert "prompt_get_completed" in event_kinds assert "prompt_get_completed" in event_kinds
def test_content_access_renders_local_documentation_prompt() -> None: async def test_content_access_renders_local_documentation_prompt() -> None:
content_access, events = _make_content_access( content_access, events = _make_content_access(
store_root="content_access_local_prompt" store_root="content_access_local_prompt"
) )
result = asyncio.run(content_access.render_prompt("test.docs.guide")) result = await content_access.render_prompt("test.docs.guide")
assert result["description"] == "Test documentation prompt." assert result["description"] == "Test documentation prompt."
assert result["messages"][0]["role"] == "user" assert result["messages"][0]["role"] == "user"
@@ -153,10 +147,10 @@ def test_content_access_renders_local_documentation_prompt() -> None:
assert "prompt_get_completed" in [e.kind for e in events.list_events()] assert "prompt_get_completed" in [e.kind for e in events.list_events()]
def test_content_access_raises_on_unknown_resource() -> None: async def test_content_access_raises_on_unknown_resource() -> None:
content_access, _ = _make_content_access( content_access, _ = _make_content_access(
store_root="content_access_missing_resource" store_root="content_access_missing_resource"
) )
with pytest.raises(KeyError): with pytest.raises(KeyError):
asyncio.run(content_access.read_resource("nonexistent.resource")) await content_access.read_resource("nonexistent.resource")
+29 -37
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from typing import Any, cast from typing import Any, cast
from wf_authoring import build_async_registry from wf_authoring import build_async_registry
@@ -19,7 +18,7 @@ from ..test_support import (
) )
def test_service_records_tool_call_events() -> None: async def test_service_records_tool_call_events() -> None:
from wf_core import END, RunStatus from wf_core import END, RunStatus
from ..test_support import input_binding, output_binding from ..test_support import input_binding, output_binding
@@ -31,7 +30,7 @@ def test_service_records_tool_call_events() -> None:
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
plan = raw_plan( plan = raw_plan(
name="tool_event_plan", name="tool_event_plan",
@@ -59,7 +58,7 @@ def test_service_records_tool_call_events() -> None:
edges=[{"from": "echo", "outcome": "ok", "to": END}], edges=[{"from": "echo", "outcome": "ok", "to": END}],
) )
run = asyncio.run(service.run_workflow_from_plan(plan, {"text": "hello"})) run = await service.run_workflow_from_plan(plan, {"text": "hello"})
assert run.status == RunStatus.COMPLETED assert run.status == RunStatus.COMPLETED
tool_events = [ tool_events = [
@@ -73,7 +72,7 @@ def test_service_records_tool_call_events() -> None:
assert tool_events[1].payload["outcome"] == "ok" assert tool_events[1].payload["outcome"] == "ok"
def test_service_rejects_text_binding_for_raw_mcp_content_contract() -> None: async def test_service_rejects_text_binding_for_raw_mcp_content_contract() -> None:
from wf_core import END from wf_core import END
from ..test_support import input_binding, output_binding from ..test_support import input_binding, output_binding
@@ -84,7 +83,7 @@ def test_service_rejects_text_binding_for_raw_mcp_content_contract() -> None:
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", ContentOnlyOutputAdapter()) service.register_adapter("demo", ContentOnlyOutputAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
plan = raw_plan( plan = raw_plan(
name="raw_content_contract", name="raw_content_contract",
input_schema={ input_schema={
@@ -127,14 +126,14 @@ def test_service_rejects_text_binding_for_raw_mcp_content_contract() -> None:
) )
def test_service_can_inspect_resources_and_prompts() -> None: async def test_service_can_inspect_resources_and_prompts() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "inspect_store")) service = WfMcpService(store=FileStore(local_temp_root() / "inspect_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
resources = service.list_resources(connection_id="demo.personal") resources = service.list_resources(connection_id="demo.personal")
prompts = service.list_prompts(connection_id="demo.personal") prompts = service.list_prompts(connection_id="demo.personal")
@@ -153,7 +152,7 @@ def test_service_can_inspect_resources_and_prompts() -> None:
assert prompt.arguments[0]["name"] == "text" assert prompt.arguments[0]["name"] == "text"
def test_service_reports_connection_statuses() -> None: async def test_service_reports_connection_statuses() -> None:
import shutil import shutil
store = local_temp_root() / "status_store" store = local_temp_root() / "status_store"
@@ -180,7 +179,7 @@ def test_service_reports_connection_statuses() -> None:
} }
] ]
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
after = service.connection_statuses() after = service.connection_statuses()
assert after[0]["has_snapshot"] is True assert after[0]["has_snapshot"] is True
assert after[0]["node_count"] == 1 assert after[0]["node_count"] == 1
@@ -188,7 +187,7 @@ def test_service_reports_connection_statuses() -> None:
assert after[0]["prompt_count"] == 1 assert after[0]["prompt_count"] == 1
def test_service_can_proxy_resource_reads_and_prompt_gets() -> None: async def test_service_can_proxy_resource_reads_and_prompt_gets() -> None:
import shutil import shutil
store = local_temp_root() / "proxy_store" store = local_temp_root() / "proxy_store"
@@ -199,16 +198,12 @@ def test_service_can_proxy_resource_reads_and_prompt_gets() -> None:
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
resource_result = asyncio.run( resource_result = await service.read_resource("demo.personal.resource.welcome")
service.read_resource("demo.personal.resource.welcome") prompt_result = await service.render_prompt(
) "demo.personal.prompt.summarize",
prompt_result = asyncio.run( arguments={"text": "hello world"},
service.render_prompt(
"demo.personal.prompt.summarize",
arguments={"text": "hello world"},
)
) )
assert ( assert (
@@ -227,22 +222,20 @@ def test_service_can_proxy_resource_reads_and_prompt_gets() -> None:
assert "prompt_get_completed" in event_kinds assert "prompt_get_completed" in event_kinds
def test_service_can_invoke_raw_method_and_notification() -> None: async def test_service_can_invoke_raw_method_and_notification() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "raw_store")) service = WfMcpService(store=FileStore(local_temp_root() / "raw_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
result = asyncio.run( result = await service.invoke_method(
service.invoke_method("demo.personal", "demo.echo", params={"text": "hello"}) "demo.personal", "demo.echo", params={"text": "hello"}
) )
asyncio.run( await service.send_notification(
service.send_notification( "demo.personal",
"demo.personal", "notifications/progress",
"notifications/progress", params={"progress": 1},
params={"progress": 1},
)
) )
assert result == {"echoed": "hello"} assert result == {"echoed": "hello"}
@@ -253,7 +246,7 @@ def test_service_can_invoke_raw_method_and_notification() -> None:
assert "raw_notification_completed" in event_kinds assert "raw_notification_completed" in event_kinds
def test_generated_specs_use_injected_tool_executor() -> None: async def test_generated_specs_use_injected_tool_executor() -> None:
class RecordingExecutor: class RecordingExecutor:
def __init__(self) -> None: def __init__(self) -> None:
self.payloads: list[dict[str, Any]] = [] self.payloads: list[dict[str, Any]] = []
@@ -278,21 +271,20 @@ def test_generated_specs_use_injected_tool_executor() -> None:
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
spec = service._get_qualified_spec("demo.personal.echo_tool") spec = service._get_qualified_spec("demo.personal.echo_tool")
handler = build_async_registry(spec)[spec.name] handler = build_async_registry(spec)[spec.name]
async def run_node() -> dict[str, Any]: result = await handler(
return await handler({"text": "hello"}, RuntimeContext(current_node_id="echo")) {"text": "hello"}, RuntimeContext(current_node_id="echo")
)
result = asyncio.run(run_node())
assert result["outcome"] == "ok" assert result["outcome"] == "ok"
assert result["output"]["echoed"] == "hello" assert result["output"]["echoed"] == "hello"
assert executor.payloads == [{"text": "hello"}] assert executor.payloads == [{"text": "hello"}]
def test_service_records_catalog_refresh_failures() -> None: async def test_service_records_catalog_refresh_failures() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "refresh_fail_store")) service = WfMcpService(store=FileStore(local_temp_root() / "refresh_fail_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -300,7 +292,7 @@ def test_service_records_catalog_refresh_failures() -> None:
service.register_adapter("demo", FailingDiscoveryAdapter()) service.register_adapter("demo", FailingDiscoveryAdapter())
try: try:
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
except PermissionError as exc: except PermissionError as exc:
assert str(exc) == "Access is denied" assert str(exc) == "Access is denied"
else: else:
+22 -26
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from wf_authoring import NodeSpec from wf_authoring import NodeSpec
from wf_mcp.broker import WfMcpService from wf_mcp.broker import WfMcpService
from wf_mcp.models import ConnectionConfig from wf_mcp.models import ConnectionConfig
@@ -21,7 +19,7 @@ from ..test_support import (
from .conftest import raw_plan, single_echo_plan from .conftest import raw_plan, single_echo_plan
def test_service_compiles_and_runs_raw_plan() -> None: async def test_service_compiles_and_runs_raw_plan() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "run_store")) service = WfMcpService(store=FileStore(local_temp_root() / "run_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -64,13 +62,13 @@ def test_service_compiles_and_runs_raw_plan() -> None:
edges=[{"from": "echo", "outcome": "ok", "to": "__end__"}], edges=[{"from": "echo", "outcome": "ok", "to": "__end__"}],
) )
run = asyncio.run(service.run_workflow_from_plan(plan, {"text": "hello"})) run = await service.run_workflow_from_plan(plan, {"text": "hello"})
assert run.status == "completed" assert run.status == "completed"
assert run.output["echoed"] == "hello" assert run.output["echoed"] == "hello"
def test_service_preserves_raw_plan_root_output_bindings() -> None: async def test_service_preserves_raw_plan_root_output_bindings() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "root_output_store")) service = WfMcpService(store=FileStore(local_temp_root() / "root_output_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -113,12 +111,12 @@ def test_service_preserves_raw_plan_root_output_bindings() -> None:
edges=[{"from": "echo", "outcome": "ok", "to": "__end__"}], edges=[{"from": "echo", "outcome": "ok", "to": "__end__"}],
) )
run = asyncio.run(service.run_workflow_from_plan(plan, {"text": "hello"})) run = await service.run_workflow_from_plan(plan, {"text": "hello"})
assert run.output["echoed"] == "hello" assert run.output["echoed"] == "hello"
def test_service_resolves_registered_spec_with_dotted_local_name() -> None: async def test_service_resolves_registered_spec_with_dotted_local_name() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "dotted_store")) service = WfMcpService(store=FileStore(local_temp_root() / "dotted_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -127,13 +125,13 @@ def test_service_resolves_registered_spec_with_dotted_local_name() -> None:
plan = single_echo_plan("dotted_plan", "demo.personal.echo_tool") plan = single_echo_plan("dotted_plan", "demo.personal.echo_tool")
run = asyncio.run(service.run_workflow_from_plan(plan, {"text": "hello"})) run = await service.run_workflow_from_plan(plan, {"text": "hello"})
assert run.status == "completed" assert run.status == "completed"
assert run.output["echoed"] == "hello" assert run.output["echoed"] == "hello"
def test_service_runs_logical_source_plan_with_dotted_local_name() -> None: async def test_service_runs_logical_source_plan_with_dotted_local_name() -> None:
import shutil import shutil
from wf_artifacts import WorkflowDeployment from wf_artifacts import WorkflowDeployment
@@ -163,26 +161,24 @@ def test_service_runs_logical_source_plan_with_dotted_local_name() -> None:
plan = single_echo_plan("logical_plan", "demo.foo.bar") plan = single_echo_plan("logical_plan", "demo.foo.bar")
run = asyncio.run( run = await service.run_workflow_from_plan(
service.run_workflow_from_plan( plan,
plan, {"text": "hello"},
{"text": "hello"}, deployment=WorkflowDeployment(
deployment=WorkflowDeployment( id="logical_dotted.personal",
id="logical_dotted.personal", artifact_id="logical_dotted",
artifact_id="logical_dotted", artifact_version=1,
artifact_version=1, bindings=[
bindings=[ {"logical_source": "demo", "concrete_source": "demo.personal"}
{"logical_source": "demo", "concrete_source": "demo.personal"} ],
], ),
),
)
) )
assert run.status == "completed" assert run.status == "completed"
assert run.output["echoed"] == "hello" assert run.output["echoed"] == "hello"
def test_service_binds_longest_logical_source_prefix_first() -> None: async def test_service_binds_longest_logical_source_prefix_first() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "prefix_store")) service = WfMcpService(store=FileStore(local_temp_root() / "prefix_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -194,7 +190,7 @@ def test_service_binds_longest_logical_source_prefix_first() -> None:
plan = single_echo_plan("prefix_plan", "demo.personal.echo_tool") plan = single_echo_plan("prefix_plan", "demo.personal.echo_tool")
run = asyncio.run(service.run_workflow_from_plan(plan, {"text": "hello"})) run = await service.run_workflow_from_plan(plan, {"text": "hello"})
assert run.status == "completed" assert run.status == "completed"
@@ -302,14 +298,14 @@ def test_service_preserves_planner_hidden_connection_source_on_reregistration()
assert "demo.personal.echo_tool" not in source.capabilities.node_specs assert "demo.personal.echo_tool" not in source.capabilities.node_specs
def test_service_refreshes_catalog_from_adapter() -> None: async def test_service_refreshes_catalog_from_adapter() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "refresh_store")) service = WfMcpService(store=FileStore(local_temp_root() / "refresh_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
service.register_adapter("demo", FakeAdapter()) service.register_adapter("demo", FakeAdapter())
asyncio.run(service.refresh_connection_catalog("demo.personal")) await service.refresh_connection_catalog("demo.personal")
source = service.capability_sources["demo.personal"] source = service.capability_sources["demo.personal"]
assert "demo.personal.echo_tool" in source.capabilities.node_specs assert "demo.personal.echo_tool" in source.capabilities.node_specs
+15 -23
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from wf_artifacts import WorkflowDeployment from wf_artifacts import WorkflowDeployment
from wf_platform import CapabilityBuckets, CapabilitySource, SourcePermissions from wf_platform import CapabilityBuckets, CapabilitySource, SourcePermissions
@@ -61,7 +59,7 @@ def test_wfmcpservice_uses_upstream_transport_for_adapters_and_auth() -> None:
assert service.list_events()[-1].kind == "auth_saved" assert service.list_events()[-1].kind == "auth_saved"
def test_upstream_transport_invokes_raw_method_and_records_events() -> None: async def test_upstream_transport_invokes_raw_method_and_records_events() -> None:
events: list[McpEvent] = [] events: list[McpEvent] = []
connections = ConnectionRegistry() connections = ConnectionRegistry()
connections.register( connections.register(
@@ -73,12 +71,10 @@ def test_upstream_transport_invokes_raw_method_and_records_events() -> None:
) )
transport.register_adapter("demo", FakeAdapter()) transport.register_adapter("demo", FakeAdapter())
result = asyncio.run( result = await transport.invoke_method(
transport.invoke_method( connections.get("demo.personal"),
connections.get("demo.personal"), "demo.echo",
"demo.echo", params={"text": "hello"},
params={"text": "hello"},
)
) )
assert result["echoed"] == "hello" assert result["echoed"] == "hello"
@@ -88,7 +84,7 @@ def test_upstream_transport_invokes_raw_method_and_records_events() -> None:
] ]
def test_upstream_transport_refreshes_catalog_directly() -> None: async def test_upstream_transport_refreshes_catalog_directly() -> None:
events: list[McpEvent] = [] events: list[McpEvent] = []
store = FileStore(local_temp_root() / "upstream_refresh") store = FileStore(local_temp_root() / "upstream_refresh")
connections = ConnectionRegistry() connections = ConnectionRegistry()
@@ -107,12 +103,10 @@ def test_upstream_transport_refreshes_catalog_directly() -> None:
) )
source_catalog.hydrate_connection_source_from_snapshot(connection) source_catalog.hydrate_connection_source_from_snapshot(connection)
asyncio.run( await transport.refresh_connection_catalog(
transport.refresh_connection_catalog( connection,
connection, source_catalog=source_catalog,
source_catalog=source_catalog, record_catalog_change_events=lambda source_id, snapshot, reason: None,
record_catalog_change_events=lambda source_id, snapshot, reason: None,
)
) )
snapshot = store.load_catalog("demo.personal") snapshot = store.load_catalog("demo.personal")
@@ -122,7 +116,7 @@ def test_upstream_transport_refreshes_catalog_directly() -> None:
assert "catalog_refresh_completed" in [event.kind for event in events] assert "catalog_refresh_completed" in [event.kind for event in events]
def test_upstream_transport_live_diagnostics_report_missing_connection() -> None: async def test_upstream_transport_live_diagnostics_report_missing_connection() -> None:
transport = UpstreamTransportService( transport = UpstreamTransportService(
store=FileStore(local_temp_root() / "upstream_live_missing"), store=FileStore(local_temp_root() / "upstream_live_missing"),
event_sink=lambda event: None, event_sink=lambda event: None,
@@ -156,12 +150,10 @@ def test_upstream_transport_live_diagnostics_report_missing_connection() -> None
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}], bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
) )
diagnostics = asyncio.run( diagnostics = await transport.deployment_diagnostics(
transport.deployment_diagnostics( deployment=deployment,
deployment=deployment, artifacts=[artifact],
artifacts=[artifact], source_catalog=source_catalog,
source_catalog=source_catalog,
)
) )
assert diagnostics[0].code == "source_unreachable" assert diagnostics[0].code == "source_unreachable"
+31 -37
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from wf_core import END, NodeUse, RunStatus from wf_core import END, NodeUse, RunStatus
from wf_mcp.broker import WfMcpService from wf_mcp.broker import WfMcpService
from wf_mcp.broker.service.source_catalog import SourceCatalogService from wf_mcp.broker.service.source_catalog import SourceCatalogService
@@ -112,7 +110,7 @@ def test_workflow_runtime_service_prepares_node_registry_and_reducers() -> None:
assert prepared_subgraphs == {} assert prepared_subgraphs == {}
def test_workflow_runtime_service_runs_plan_and_emits_events() -> None: async def test_workflow_runtime_service_runs_plan_and_emits_events() -> None:
events = [] events = []
runtime = WorkflowRuntimeService( runtime = WorkflowRuntimeService(
source_catalog=_source_catalog(), source_catalog=_source_catalog(),
@@ -120,11 +118,9 @@ def test_workflow_runtime_service_runs_plan_and_emits_events() -> None:
emit_event=events.append, emit_event=events.append,
) )
run = asyncio.run( run = await runtime.run_workflow_from_plan(
runtime.run_workflow_from_plan( single_echo_plan("runtime_run", "demo.personal.echo_tool"),
single_echo_plan("runtime_run", "demo.personal.echo_tool"), {"text": "hello"},
{"text": "hello"},
)
) )
assert run.output["echoed"] == "hello" assert run.output["echoed"] == "hello"
@@ -135,37 +131,35 @@ def test_workflow_runtime_service_runs_plan_and_emits_events() -> None:
assert events[1].payload["status"] == "completed" assert events[1].payload["status"] == "completed"
def test_workflow_runtime_service_emits_failed_event_for_failed_run() -> None: async def test_workflow_runtime_service_emits_failed_event_for_failed_run() -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "runtime_failed_event")) service = WfMcpService(store=FileStore(local_temp_root() / "runtime_failed_event"))
run = asyncio.run( run = await service.workflow_runtime.run_workflow_from_plan(
service.workflow_runtime.run_workflow_from_plan( raw_plan(
raw_plan( name="runtime_failed_event",
name="runtime_failed_event", input_schema={"type": "object", "properties": {}},
input_schema={"type": "object", "properties": {}}, state_schema={"type": "object", "properties": {}},
state_schema={"type": "object", "properties": {}}, output_schema={"type": "object", "properties": {}},
output_schema={"type": "object", "properties": {}}, start="fail",
start="fail", nodes=[
nodes=[ {
{ "id": "fail",
"id": "fail", "type": "node",
"type": "node", "node": "wf.std.runtime_error",
"node": "wf.std.runtime_error", "input": [
"input": [ {
{ "value": "boom",
"value": "boom", "target": {
"target": { "root": "local",
"root": "local", "parts": ["message"],
"parts": ["message"], },
}, }
} ],
], }
} ],
], edges=[{"from": "fail", "outcome": "ok", "to": END}],
edges=[{"from": "fail", "outcome": "ok", "to": END}], ),
), {},
{},
)
) )
assert run.status == RunStatus.FAILED assert run.status == RunStatus.FAILED
+39 -52
View File
@@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import ast import ast
import asyncio
from pathlib import Path from pathlib import Path
from wf_api.models import RawWorkflowPlan from wf_api.models import RawWorkflowPlan
@@ -74,36 +73,30 @@ def test_wf_server_context_imports_no_wfmcp_service() -> None:
assert violations == [] assert violations == []
def test_local_static_server_runs_deployment_and_persists_run(tmp_path) -> None: async def test_local_static_server_runs_deployment_and_persists_run(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
api = server.api api = server.api
plan = _constant_plan() plan = _constant_plan()
artifact_result = asyncio.run( artifact_result = await api.create_artifact_from_plan(
api.create_artifact_from_plan( artifact_id="server_constant",
artifact_id="server_constant", version=1,
version=1, title="Server Constant",
title="Server Constant", plan=plan,
plan=plan, outcomes=["ok"],
outcomes=["ok"], source_bindings={"wf.std": "wf.std"},
source_bindings={"wf.std": "wf.std"},
)
) )
deployment_result = asyncio.run( deployment_result = await api.save_deployment(
api.save_deployment( {
{ "id": "server_constant.default",
"id": "server_constant.default", "artifact_id": "server_constant",
"artifact_id": "server_constant", "artifact_version": 1,
"artifact_version": 1, "bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}], }
}
)
) )
run_result = asyncio.run( run_result = await api.run_deployment(
api.run_deployment( deployment_id="server_constant.default",
deployment_id="server_constant.default", workflow_input={},
workflow_input={},
)
) )
assert artifact_result["artifact_id"] == "server_constant" assert artifact_result["artifact_id"] == "server_constant"
@@ -116,40 +109,34 @@ def test_local_static_server_runs_deployment_and_persists_run(tmp_path) -> None:
) )
def test_local_static_server_inspects_and_reads_bounded_trace(tmp_path) -> None: async def test_local_static_server_inspects_and_reads_bounded_trace(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store") server = build_local_static_workflow_server(tmp_path / "store")
api = server.api api = server.api
plan = _constant_plan() plan = _constant_plan()
asyncio.run( await api.create_artifact_from_plan(
api.create_artifact_from_plan( artifact_id="server_trace",
artifact_id="server_trace", version=1,
version=1, title="Server Trace",
title="Server Trace", plan=plan.model_copy(update={"name": "server_trace"}),
plan=plan.model_copy(update={"name": "server_trace"}), outcomes=["ok"],
outcomes=["ok"], source_bindings={"wf.std": "wf.std"},
source_bindings={"wf.std": "wf.std"},
)
) )
asyncio.run( await api.save_deployment(
api.save_deployment( {
{ "id": "server_trace.default",
"id": "server_trace.default", "artifact_id": "server_trace",
"artifact_id": "server_trace", "artifact_version": 1,
"artifact_version": 1, "bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}],
"bindings": [{"logical_source": "wf.std", "concrete_source": "wf.std"}], }
}
)
) )
run_result = asyncio.run( run_result = await api.run_deployment(
api.run_deployment(deployment_id="server_trace.default", workflow_input={}) deployment_id="server_trace.default", workflow_input={}
) )
summary = asyncio.run(api.inspect_run(run_id=run_result["run_id"])) summary = await api.inspect_run(run_id=run_result["run_id"])
trace = asyncio.run( trace = await api.read_run_trace(
api.read_run_trace( run_id=run_result["run_id"],
run_id=run_result["run_id"], trace_range=server.trace_range(start=0, limit=1),
trace_range=server.trace_range(start=0, limit=1),
)
) )
assert "trace" not in summary assert "trace" not in summary