half an attempt to asyncify tests + deprecate old temp path creation

This commit is contained in:
lda
2026-06-11 05:01:33 +07:00 Verified
parent d695fd7533
commit 9dae4a8445
32 changed files with 782 additions and 753 deletions
+3
View File
@@ -213,3 +213,6 @@ wf*.config.json
# Generated MCPB packages # Generated MCPB packages
*.mcpb *.mcpb
# pytest files
.pytest-tmp/
+2 -1
View File
@@ -29,11 +29,12 @@ dev = [
"pytest>=8", "pytest>=8",
# "pytest-sugar>=1", # "pytest-sugar>=1",
"pytest-asyncio>=1.4.0", "pytest-asyncio>=1.4.0",
"pytest-xdist>=3.8.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 -n auto --basetemp .pytest-tmp"
pythonpath = ["."] pythonpath = ["."]
asyncio_mode = "auto" asyncio_mode = "auto"
+8 -12
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pydantic import BaseModel from pydantic import BaseModel
from wf_authoring import build_async_registry, node from wf_authoring import build_async_registry, node
@@ -33,7 +31,7 @@ class InferredAsyncOutput(BaseModel):
echoed: str echoed: str
def test_async_registry_accepts_sync_and_async_specs() -> None: async def test_async_registry_accepts_sync_and_async_specs() -> None:
@node() @node()
def sync_echo( def sync_echo(
payload: InferredEchoInput, payload: InferredEchoInput,
@@ -54,14 +52,14 @@ def test_async_registry_accepts_sync_and_async_specs() -> None:
async def run_handler(name: str, value: str) -> dict[str, object]: async def run_handler(name: str, value: str) -> dict[str, object]:
return await registry[name]({"value": value}, ctx) return await registry[name]({"value": value}, ctx)
sync_result = asyncio.run(run_handler("sync_echo", "hello")) sync_result = await run_handler("sync_echo", "hello")
async_result = asyncio.run(run_handler("async_echo", "world")) async_result = await run_handler("async_echo", "world")
assert sync_result == {"outcome": "ok", "output": {"echoed": "hello"}} assert sync_result == {"outcome": "ok", "output": {"echoed": "hello"}}
assert async_result == {"outcome": "ok", "output": {"echoed": "async:world"}} assert async_result == {"outcome": "ok", "output": {"echoed": "async:world"}}
def test_execute_workflow_async_runs_with_async_registry() -> None: async def test_execute_workflow_async_runs_with_async_registry() -> None:
workflow, _ = build_authoring_demo_workflow() workflow, _ = build_authoring_demo_workflow()
registry = build_async_registry( registry = build_async_registry(
drive_list_files_spec, drive_list_files_spec,
@@ -71,12 +69,10 @@ def test_execute_workflow_async_runs_with_async_registry() -> None:
mark_email_skipped_spec, mark_email_skipped_spec,
) )
run = asyncio.run( run = await execute_workflow_async(
execute_workflow_async( workflow,
workflow, {"folder_id": "demo-folder", "should_email": False},
{"folder_id": "demo-folder", "should_email": False}, registry,
registry,
)
) )
assert run.status == RunStatus.COMPLETED assert run.status == RunStatus.COMPLETED
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from typing import Any, cast from typing import Any, cast
import pytest import pytest
@@ -33,8 +32,8 @@ def test_authoring_concurrent_foreach_collects_item_errors() -> None:
assert error["item"] == "bad" assert error["item"] == "bad"
def test_authoring_async_concurrent_foreach_commits_in_item_order() -> None: async def test_authoring_async_concurrent_foreach_commits_in_item_order() -> None:
run = asyncio.run(run_async_ordered_example()) run = await run_async_ordered_example()
assert run.status == RunStatus.COMPLETED assert run.status == RunStatus.COMPLETED
assert run.output["seen"] == ["a", "b", "c"] assert run.output["seen"] == ["a", "b", "c"]
+5 -9
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pydantic import BaseModel from pydantic import BaseModel
from examples.authoring_workflow_as_node import ( from examples.authoring_workflow_as_node import (
@@ -63,7 +61,7 @@ def test_subgraph_node_wraps_compiled_workflow() -> None:
assert "summary" in result["output"] assert "summary" in result["output"]
def test_async_subgraph_node_wraps_async_compiled_workflow() -> None: async def test_async_subgraph_node_wraps_async_compiled_workflow() -> None:
class ChildInput(BaseModel): class ChildInput(BaseModel):
text: str text: str
@@ -114,12 +112,10 @@ def test_async_subgraph_node_wraps_async_compiled_workflow() -> None:
parent.set_entry_point(step) parent.set_entry_point(step)
parent.connect(step, "ok", END) parent.connect(step, "ok", END)
run = asyncio.run( run = await execute_workflow_async(
execute_workflow_async( parent.compile(),
parent.compile(), {"text": "hello"},
{"text": "hello"}, build_async_registry(wrapped),
build_async_registry(wrapped),
)
) )
assert run.status == RunStatus.COMPLETED assert run.status == RunStatus.COMPLETED
+2 -10
View File
@@ -18,11 +18,7 @@ from wf_core import (
) )
def test_async_concurrent_foreach_respects_max_active() -> None: async def test_async_concurrent_foreach_respects_max_active() -> None:
asyncio.run(_assert_async_concurrent_foreach_respects_max_active())
async def _assert_async_concurrent_foreach_respects_max_active() -> None:
workflow = _workflow(max_active=2) workflow = _workflow(max_active=2)
active = 0 active = 0
max_seen = 0 max_seen = 0
@@ -45,11 +41,7 @@ async def _assert_async_concurrent_foreach_respects_max_active() -> None:
assert run.state["seen"] == ["a", "b", "c", "d"] assert run.state["seen"] == ["a", "b", "c", "d"]
def test_async_concurrent_foreach_commits_in_item_index_order() -> None: async def test_async_concurrent_foreach_commits_in_item_index_order() -> None:
asyncio.run(_assert_async_concurrent_foreach_commits_in_item_index_order())
async def _assert_async_concurrent_foreach_commits_in_item_index_order() -> None:
workflow = _workflow(max_active=3) workflow = _workflow(max_active=3)
async def record(payload: dict[str, Any], _ctx: object) -> dict[str, Any]: async def record(payload: dict[str, Any], _ctx: object) -> dict[str, Any]:
@@ -21,11 +21,7 @@ from wf_core import (
) )
def test_concurrent_foreach_interrupt_returns_before_refill() -> None: async def test_concurrent_foreach_interrupt_returns_before_refill() -> None:
asyncio.run(_assert_concurrent_foreach_interrupt_returns_before_refill())
async def _assert_concurrent_foreach_interrupt_returns_before_refill() -> None:
run = await execute_workflow_async( run = await execute_workflow_async(
_workflow(), _workflow(),
{"items": ["a", "b", "c"]}, {"items": ["a", "b", "c"]},
@@ -40,11 +36,7 @@ async def _assert_concurrent_foreach_interrupt_returns_before_refill() -> None:
assert "seen" not in run.state assert "seen" not in run.state
def test_resume_prioritizes_interrupted_item_before_siblings() -> None: async def test_resume_prioritizes_interrupted_item_before_siblings() -> None:
asyncio.run(_assert_resume_prioritizes_interrupted_item_before_siblings())
async def _assert_resume_prioritizes_interrupted_item_before_siblings() -> None:
workflow = _workflow() workflow = _workflow()
run = await execute_workflow_async( run = await execute_workflow_async(
workflow, workflow,
+19 -28
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from typing import Any from typing import Any
import pytest import pytest
@@ -27,54 +26,46 @@ async def explode(_payload: dict[str, Any], _context: RuntimeContext) -> dict[st
raise ValueError("boom") raise ValueError("boom")
def test_execute_result_api_returns_failed_state_without_changing_strict_execute() -> ( @pytest.mark.asyncio
async def test_execute_result_api_returns_failed_state_without_changing_strict_execute() -> (
None None
): ):
workflow = _failing_workflow() workflow = _failing_workflow()
failed = asyncio.run( failed = await execute_workflow_result_async(workflow, {}, {"explode": explode})
execute_workflow_result_async(workflow, {}, {"explode": explode})
)
assert failed.status is RunStatus.FAILED assert failed.status is RunStatus.FAILED
assert failed.error == "boom" assert failed.error == "boom"
with pytest.raises(ValueError, match="boom"): with pytest.raises(ValueError, match="boom"):
asyncio.run(execute_workflow_async(workflow, {}, {"explode": explode})) await execute_workflow_async(workflow, {}, {"explode": explode})
def test_resume_result_api_returns_failed_state_without_changing_strict_resume() -> ( @pytest.mark.asyncio
async def test_resume_result_api_returns_failed_state_without_changing_strict_resume() -> (
None None
): ):
workflow = _interrupt_then_fail_workflow() workflow = _interrupt_then_fail_workflow()
interrupted = asyncio.run( interrupted = await execute_workflow_async(workflow, {}, {"explode": explode})
execute_workflow_async(workflow, {}, {"explode": explode})
failed = await resume_workflow_result_async(
workflow,
interrupted,
{"explode": explode},
resume_payload={},
) )
failed = asyncio.run( assert failed.status is RunStatus.FAILED
resume_workflow_result_async( assert failed.error == "boom"
interrupted = await execute_workflow_async(workflow, {}, {"explode": explode})
with pytest.raises(ValueError, match="boom"):
await resume_workflow_async(
workflow, workflow,
interrupted, interrupted,
{"explode": explode}, {"explode": explode},
resume_payload={}, resume_payload={},
) )
)
assert failed.status is RunStatus.FAILED
assert failed.error == "boom"
interrupted = asyncio.run(
execute_workflow_async(workflow, {}, {"explode": explode})
)
with pytest.raises(ValueError, match="boom"):
asyncio.run(
resume_workflow_async(
workflow,
interrupted,
{"explode": explode},
resume_payload={},
)
)
def _failing_workflow() -> Workflow: def _failing_workflow() -> Workflow:
+6 -6
View File
@@ -1,7 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import pytest import pytest
from wf_core import ( from wf_core import (
@@ -187,7 +185,8 @@ def test_subgraph_step_projects_child_context_output() -> None:
assert run.output["answer"] == "root:subgraph:child" assert run.output["answer"] == "root:subgraph:child"
def test_subgraph_step_executes_prepared_async_child() -> None: @pytest.mark.asyncio
async def test_subgraph_step_executes_prepared_async_child() -> None:
async def answer(payload: dict[str, object], _ctx: object) -> dict[str, object]: async def answer(payload: dict[str, object], _ctx: object) -> dict[str, object]:
return {"answer": f"async:{payload['text']}"} return {"answer": f"async:{payload['text']}"}
@@ -204,7 +203,7 @@ def test_subgraph_step_executes_prepared_async_child() -> None:
}, },
) )
run = asyncio.run(execute()) run = await execute()
assert run.output["answer"] == "async:hello" assert run.output["answer"] == "async:hello"
assert run.trace[-1].step_type == "subgraph" assert run.trace[-1].step_type == "subgraph"
@@ -295,7 +294,8 @@ def test_subgraph_step_interrupts_and_resumes_inside_prepared_child() -> None:
assert resumed.state["answer"] == "yes" assert resumed.state["answer"] == "yes"
def test_subgraph_step_resumes_interrupted_async_prepared_child() -> None: @pytest.mark.asyncio
async def test_subgraph_step_resumes_interrupted_async_prepared_child() -> None:
async def run_child() -> RunState: async def run_child() -> RunState:
child = _interrupting_child_workflow() child = _interrupting_child_workflow()
parent = _workflow(output_schema=_schema({"answer": {"type": "string"}})) parent = _workflow(output_schema=_schema({"answer": {"type": "string"}}))
@@ -314,7 +314,7 @@ def test_subgraph_step_resumes_interrupted_async_prepared_child() -> None:
subgraphs={"child.workflow": prepared}, subgraphs={"child.workflow": prepared},
) )
resumed = asyncio.run(run_child()) resumed = await run_child()
assert resumed.status == "completed" assert resumed.status == "completed"
assert resumed.output["answer"] == "async yes" assert resumed.output["answer"] == "async yes"
+26 -15
View File
@@ -1,9 +1,9 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pathlib import Path from pathlib import Path
import httpx import httpx
import pytest
from wf_authoring import NodeReturn from wf_authoring import NodeReturn
from wf_openapi.executor import ( from wf_openapi.executor import (
@@ -17,7 +17,8 @@ from wf_openapi.validation import load_openapi_app
FIXTURE = Path(__file__).parent / "fixtures" / "petstore_minimal.openapi.json" FIXTURE = Path(__file__).parent / "fixtures" / "petstore_minimal.openapi.json"
def test_call_openapi_operation_maps_success() -> None: @pytest.mark.asyncio
async def test_call_openapi_operation_maps_success() -> None:
app = load_openapi_app(FIXTURE) app = load_openapi_app(FIXTURE)
operation = next( operation = next(
op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet" op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet"
@@ -38,14 +39,15 @@ def test_call_openapi_operation_maps_success() -> None:
client=client, client=client,
) )
result = asyncio.run(run()) result = await run()
assert result.outcome == "ok" assert result.outcome == "ok"
assert result.output.status_code == 200 assert result.output.status_code == 200
assert result.output.body["id"] == "pet-1" assert result.output.body["id"] == "pet-1"
def test_call_openapi_operation_maps_declared_http_error() -> None: @pytest.mark.asyncio
async def test_call_openapi_operation_maps_declared_http_error() -> None:
app = load_openapi_app(FIXTURE) app = load_openapi_app(FIXTURE)
operation = next( operation = next(
op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet" op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet"
@@ -65,14 +67,15 @@ def test_call_openapi_operation_maps_declared_http_error() -> None:
client=client, client=client,
) )
result = asyncio.run(run()) result = await run()
assert result.outcome == "http_error" assert result.outcome == "http_error"
assert result.output.status_code == 404 assert result.output.status_code == 404
assert result.output.body["message"] == "missing" assert result.output.body["message"] == "missing"
def test_call_openapi_operation_maps_unexpected_status() -> None: @pytest.mark.asyncio
async def test_call_openapi_operation_maps_unexpected_status() -> None:
app = load_openapi_app(FIXTURE) app = load_openapi_app(FIXTURE)
operation = next( operation = next(
op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet" op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet"
@@ -92,14 +95,17 @@ def test_call_openapi_operation_maps_unexpected_status() -> None:
client=client, client=client,
) )
result = asyncio.run(run()) result = await run()
assert result.outcome == "unexpected_status" assert result.outcome == "unexpected_status"
assert result.output.status_code == 418 assert result.output.status_code == 418
assert result.output.validation_errors assert result.output.validation_errors
def test_call_openapi_operation_maps_invalid_request_to_validation_error() -> None: @pytest.mark.asyncio
async def test_call_openapi_operation_maps_invalid_request_to_validation_error() -> (
None
):
app = load_openapi_app(FIXTURE) app = load_openapi_app(FIXTURE)
operation = next( operation = next(
op for op in load_openapi_operations(FIXTURE) if op.name == "create_pet" op for op in load_openapi_operations(FIXTURE) if op.name == "create_pet"
@@ -118,14 +124,17 @@ def test_call_openapi_operation_maps_invalid_request_to_validation_error() -> No
client=client, client=client,
) )
result = asyncio.run(run()) result = await run()
assert result.outcome == "validation_error" assert result.outcome == "validation_error"
assert result.output.status_code == 0 assert result.output.status_code == 0
assert result.output.validation_errors assert result.output.validation_errors
def test_call_openapi_operation_maps_invalid_response_to_validation_error() -> None: @pytest.mark.asyncio
async def test_call_openapi_operation_maps_invalid_response_to_validation_error() -> (
None
):
app = load_openapi_app(FIXTURE) app = load_openapi_app(FIXTURE)
operation = next( operation = next(
op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet" op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet"
@@ -145,14 +154,15 @@ def test_call_openapi_operation_maps_invalid_response_to_validation_error() -> N
client=client, client=client,
) )
result = asyncio.run(run()) result = await run()
assert result.outcome == "validation_error" assert result.outcome == "validation_error"
assert result.output.status_code == 200 assert result.output.status_code == 200
assert result.output.validation_errors assert result.output.validation_errors
def test_call_openapi_operation_maps_malformed_json_response_to_validation_error() -> ( @pytest.mark.asyncio
async def test_call_openapi_operation_maps_malformed_json_response_to_validation_error() -> (
None None
): ):
app = load_openapi_app(FIXTURE) app = load_openapi_app(FIXTURE)
@@ -178,14 +188,15 @@ def test_call_openapi_operation_maps_malformed_json_response_to_validation_error
client=client, client=client,
) )
result = asyncio.run(run()) result = await run()
assert result.outcome == "validation_error" assert result.outcome == "validation_error"
assert result.output.status_code == 200 assert result.output.status_code == 200
assert result.output.validation_errors assert result.output.validation_errors
def test_call_openapi_operation_maps_transport_error() -> None: @pytest.mark.asyncio
async def test_call_openapi_operation_maps_transport_error() -> None:
app = load_openapi_app(FIXTURE) app = load_openapi_app(FIXTURE)
operation = next( operation = next(
op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet" op for op in load_openapi_operations(FIXTURE) if op.name == "get_pet"
@@ -205,7 +216,7 @@ def test_call_openapi_operation_maps_transport_error() -> None:
client=client, client=client,
) )
result = asyncio.run(run()) result = await run()
assert result.outcome == "transport_error" assert result.outcome == "transport_error"
assert result.output.status_code == 0 assert result.output.status_code == 0
+9 -7
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any from typing import Any
@@ -55,11 +54,12 @@ class FakeAdminProvider:
return self.events return self.events
def test_admin_api_lists_connections_in_id_order() -> None: @pytest.mark.asyncio
async def test_admin_api_lists_connections_in_id_order() -> None:
provider = FakeAdminProvider() provider = FakeAdminProvider()
api = WorkflowAdminApi(connections=provider, events=provider) api = WorkflowAdminApi(connections=provider, events=provider)
payload = asyncio.run(api.list_connections()) payload = await api.list_connections()
assert payload["total"] == 2 assert payload["total"] == 2
assert [connection["id"] for connection in payload["connections"]] == [ assert [connection["id"] for connection in payload["connections"]] == [
@@ -68,11 +68,12 @@ def test_admin_api_lists_connections_in_id_order() -> None:
] ]
def test_admin_api_lists_connection_statuses_in_id_order() -> None: @pytest.mark.asyncio
async def test_admin_api_lists_connection_statuses_in_id_order() -> None:
provider = FakeAdminProvider() provider = FakeAdminProvider()
api = WorkflowAdminApi(connections=provider, events=provider) api = WorkflowAdminApi(connections=provider, events=provider)
payload = asyncio.run(api.get_connection_statuses()) payload = await api.get_connection_statuses()
assert payload["total"] == 2 assert payload["total"] == 2
assert [status["connection_id"] for status in payload["statuses"]] == [ assert [status["connection_id"] for status in payload["statuses"]] == [
@@ -81,11 +82,12 @@ def test_admin_api_lists_connection_statuses_in_id_order() -> None:
] ]
def test_admin_api_lists_events() -> None: @pytest.mark.asyncio
async def test_admin_api_lists_events() -> None:
provider = FakeAdminProvider() provider = FakeAdminProvider()
api = WorkflowAdminApi(connections=provider, events=provider) api = WorkflowAdminApi(connections=provider, events=provider)
payload = asyncio.run(api.list_events()) payload = await api.list_events()
assert payload["total"] == 1 assert payload["total"] == 1
assert payload["events"][0]["kind"] == "connection_registered" assert payload["events"][0]["kind"] == "connection_registered"
+42 -47
View File
@@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from dataclasses import replace from dataclasses import replace
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -134,11 +133,12 @@ def _artifact_api(
return WorkflowArtifactApi(context), service return WorkflowArtifactApi(context), service
def test_save_artifact_stores_and_returns_saved(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_save_artifact_stores_and_returns_saved(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_save") artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_save")
api, _service = _artifact_api(artifact_store) api, _service = _artifact_api(artifact_store)
result = asyncio.run(api.save_artifact(_echo_artifact().model_dump(mode="json"))) result = await api.save_artifact(_echo_artifact().model_dump(mode="json"))
assert result["saved"] is True assert result["saved"] is True
assert result["artifact_id"] == "echo" assert result["artifact_id"] == "echo"
@@ -147,7 +147,8 @@ def test_save_artifact_stores_and_returns_saved(tmp_path: Path) -> None:
assert saved.id == "echo" assert saved.id == "echo"
def test_list_artifacts_returns_empty_page_without_artifact_store( @pytest.mark.asyncio
async def test_list_artifacts_returns_empty_page_without_artifact_store(
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_no_store") artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_no_store")
@@ -155,27 +156,26 @@ def test_list_artifacts_returns_empty_page_without_artifact_store(
context = replace(context_from_service(service), artifact_store=None) context = replace(context_from_service(service), artifact_store=None)
api = WorkflowArtifactApi(context) api = WorkflowArtifactApi(context)
result = asyncio.run(api.list_artifacts()) result = await api.list_artifacts()
assert result["nodes"] == [] assert result["nodes"] == []
assert result["next_cursor"] is None assert result["next_cursor"] is None
assert result["total"] == 0 assert result["total"] == 0
def test_create_artifact_from_plan_saves_with_observed_node_specs( @pytest.mark.asyncio
async def test_create_artifact_from_plan_saves_with_observed_node_specs(
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_from_plan") artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_from_plan")
api, _service = _artifact_api(artifact_store, register_echo=True) api, _service = _artifact_api(artifact_store, register_echo=True)
result = asyncio.run( result = await api.create_artifact_from_plan(
api.create_artifact_from_plan( artifact_id="echo",
artifact_id="echo", version=1,
version=1, title="Echo",
title="Echo", plan=_echo_artifact().plan,
plan=_echo_artifact().plan, outcomes=("completed",),
outcomes=("completed",),
)
) )
assert result["saved"] is True assert result["saved"] is True
@@ -184,7 +184,7 @@ def test_create_artifact_from_plan_saves_with_observed_node_specs(
assert saved.id == "echo" assert saved.id == "echo"
def test_create_artifact_from_workspace_returns_saved_false_when_invalid( async def test_create_artifact_from_workspace_returns_saved_false_when_invalid(
tmp_path: Path, tmp_path: Path,
) -> None: ) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_workspace_invalid") artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_workspace_invalid")
@@ -195,49 +195,42 @@ def test_create_artifact_from_workspace_returns_saved_false_when_invalid(
context = context_from_service(service) context = context_from_service(service)
drafts_api = WorkflowDraftApi(context) drafts_api = WorkflowDraftApi(context)
asyncio.run( await drafts_api.create_draft_workspace(
drafts_api.create_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", draft=draft,
draft=draft,
)
) )
result = asyncio.run( result = await api.create_artifact_from_workspace(
api.create_artifact_from_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", artifact_id="echo",
artifact_id="echo", version=1,
version=1, title="Echo",
title="Echo", outcomes=("completed",),
outcomes=("completed",),
)
) )
assert result["saved"] is False assert result["saved"] is False
assert result["status"] == "invalid" assert result["status"] == "invalid"
def test_create_wrapper_from_workspace_saves_kind_wrapper(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_create_wrapper_from_workspace_saves_kind_wrapper(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_wrapper_workspace") artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_wrapper_workspace")
api, service = _artifact_api(artifact_store, register_echo=True) api, service = _artifact_api(artifact_store, register_echo=True)
from wf_api.drafts import WorkflowDraftApi from wf_api.drafts import WorkflowDraftApi
context = context_from_service(service) context = context_from_service(service)
drafts_api = WorkflowDraftApi(context) drafts_api = WorkflowDraftApi(context)
asyncio.run( await drafts_api.create_draft_workspace(
drafts_api.create_draft_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", draft=_echo_draft(),
draft=_echo_draft(),
)
) )
result = asyncio.run( result = await api.create_wrapper_from_workspace(
api.create_wrapper_from_workspace( workspace_id="echo_ws",
workspace_id="echo_ws", artifact_id="echo_wrapper",
artifact_id="echo_wrapper", version=1,
version=1, title="Echo Wrapper",
title="Echo Wrapper", outcomes=("completed",),
outcomes=("completed",),
)
) )
assert result["saved"] is True assert result["saved"] is True
@@ -245,12 +238,13 @@ def test_create_wrapper_from_workspace_saves_kind_wrapper(tmp_path: Path) -> Non
assert saved.kind == "wrapper" assert saved.kind == "wrapper"
def test_inspect_artifact_returns_stable_fields(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_inspect_artifact_returns_stable_fields(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_inspect") artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_inspect")
api, _service = _artifact_api(artifact_store) api, _service = _artifact_api(artifact_store)
artifact_store.save_artifact(_echo_artifact()) artifact_store.save_artifact(_echo_artifact())
result = asyncio.run(api.inspect_artifact(artifact_id="echo", version=1)) result = await api.inspect_artifact(artifact_id="echo", version=1)
assert result["id"] == "echo" assert result["id"] == "echo"
assert result["version"] == 1 assert result["version"] == 1
@@ -258,7 +252,8 @@ def test_inspect_artifact_returns_stable_fields(tmp_path: Path) -> None:
assert "plan" in result assert "plan" in result
def test_handler_delegation_for_inspect_artifact(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_handler_delegation_for_inspect_artifact(tmp_path: Path) -> None:
"""WorkflowSurfaceHandlers.inspect_artifact delegates to WorkflowArtifactApi.""" """WorkflowSurfaceHandlers.inspect_artifact delegates to WorkflowArtifactApi."""
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_delegation") artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_delegation")
mcp_root = artifact_store.root / "delegation_mcp" mcp_root = artifact_store.root / "delegation_mcp"
@@ -273,8 +268,8 @@ def test_handler_delegation_for_inspect_artifact(tmp_path: Path) -> None:
context = context_from_service(service) context = context_from_service(service)
api = WorkflowArtifactApi(context) api = WorkflowArtifactApi(context)
handler_result = asyncio.run(h.inspect_artifact(artifact_id="echo", version=1)) handler_result = await h.inspect_artifact(artifact_id="echo", version=1)
api_result = asyncio.run(api.inspect_artifact(artifact_id="echo", version=1)) api_result = await api.inspect_artifact(artifact_id="echo", version=1)
assert handler_result["id"] == api_result["id"] assert handler_result["id"] == api_result["id"]
assert handler_result["version"] == api_result["version"] assert handler_result["version"] == api_result["version"]
+50 -49
View File
@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -42,11 +41,14 @@ def _capability_api(
return WorkflowCapabilityApi(context), service return WorkflowCapabilityApi(context), service
def test_list_capabilities_returns_planner_visible_sources(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_list_capabilities_returns_planner_visible_sources(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_list") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_list")
api, _service = _capability_api(artifact_store, register_echo=True) api, _service = _capability_api(artifact_store, register_echo=True)
result = asyncio.run(api.list_capabilities()) result = await api.list_capabilities()
assert result["total"] >= 1 assert result["total"] >= 1
assert any( assert any(
@@ -62,22 +64,24 @@ def test_list_capabilities_returns_planner_visible_sources(tmp_path: Path) -> No
assert "output_fields" in first assert "output_fields" in first
def test_list_capabilities_filters_by_source(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_list_capabilities_filters_by_source(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_list_filter") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_list_filter")
api, _service = _capability_api(artifact_store, register_echo=True) api, _service = _capability_api(artifact_store, register_echo=True)
result = asyncio.run(api.list_capabilities(source_id="wf.std", query="truthy")) result = await api.list_capabilities(source_id="wf.std", query="truthy")
assert [item["name"] for item in result["capabilities"]] == ["wf.std.truthy"] assert [item["name"] for item in result["capabilities"]] == ["wf.std.truthy"]
def test_inspect_capability_returns_detail_with_wrapper_hints(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_inspect_capability_returns_detail_with_wrapper_hints(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_inspect") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_inspect")
api, _service = _capability_api(artifact_store, register_echo=True) api, _service = _capability_api(artifact_store, register_echo=True)
detail = asyncio.run( detail = await api.inspect_capability(qualified_name="demo.personal.echo_tool")
api.inspect_capability(qualified_name="demo.personal.echo_tool")
)
assert detail["name"] == "demo.personal.echo_tool" assert detail["name"] == "demo.personal.echo_tool"
assert "wrapper_hints" in detail assert "wrapper_hints" in detail
@@ -87,23 +91,23 @@ def test_inspect_capability_returns_detail_with_wrapper_hints(tmp_path: Path) ->
assert hints["output_map"] == {"echoed": "state.echoed"} assert hints["output_map"] == {"echoed": "state.echoed"}
def test_inspect_capability_raises_on_unknown(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_inspect_capability_raises_on_unknown(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_inspect_unknown") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_inspect_unknown")
api, _service = _capability_api(artifact_store, register_echo=True) api, _service = _capability_api(artifact_store, register_echo=True)
with pytest.raises(KeyError, match="no.such.capability"): with pytest.raises(KeyError, match="no.such.capability"):
asyncio.run(api.inspect_capability(qualified_name="no.such.capability")) await api.inspect_capability(qualified_name="no.such.capability")
def test_call_capability_node_spec_success(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_call_capability_node_spec_success(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_call") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_call")
api, _service = _capability_api(artifact_store, register_echo=True) api, _service = _capability_api(artifact_store, register_echo=True)
result = asyncio.run( result = await api.call_capability(
api.call_capability( qualified_name="demo.personal.echo_tool",
qualified_name="demo.personal.echo_tool", payload={"text": "hello"},
payload={"text": "hello"},
)
) )
assert result["kind"] == "node_spec" assert result["kind"] == "node_spec"
@@ -113,15 +117,14 @@ def test_call_capability_node_spec_success(tmp_path: Path) -> None:
assert result["deployment_id"] is None assert result["deployment_id"] is None
def test_call_capability_node_spec_failure(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_call_capability_node_spec_failure(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_call_fail") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_call_fail")
api, _service = _capability_api(artifact_store, register_failing=True) api, _service = _capability_api(artifact_store, register_failing=True)
result = asyncio.run( result = await api.call_capability(
api.call_capability( qualified_name="demo.personal.failing_tool",
qualified_name="demo.personal.failing_tool", payload={"message": "boom"},
payload={"message": "boom"},
)
) )
assert result["kind"] == "node_spec" assert result["kind"] == "node_spec"
@@ -130,7 +133,8 @@ def test_call_capability_node_spec_failure(tmp_path: Path) -> None:
assert result["diagnostics"][0]["code"] == "capability_call_failed" assert result["diagnostics"][0]["code"] == "capability_call_failed"
def test_list_capabilities_includes_saved_wrapper(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_list_capabilities_includes_saved_wrapper(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_wrapper_list") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_wrapper_list")
artifact_store.save_artifact( artifact_store.save_artifact(
echo_artifact().model_copy( echo_artifact().model_copy(
@@ -143,7 +147,7 @@ def test_list_capabilities_includes_saved_wrapper(tmp_path: Path) -> None:
) )
api, _service = _capability_api(artifact_store) api, _service = _capability_api(artifact_store)
result = asyncio.run(api.list_capabilities(source_id="workflow", query="echo")) result = await api.list_capabilities(source_id="workflow", query="echo")
names = [item["name"] for item in result["capabilities"]] names = [item["name"] for item in result["capabilities"]]
assert names == ["workflow.echo_wrapper.v1"] assert names == ["workflow.echo_wrapper.v1"]
@@ -159,16 +163,15 @@ def test_list_capabilities_includes_saved_wrapper(tmp_path: Path) -> None:
assert row["output_fields"] == ["echoed"] assert row["output_fields"] == ["echoed"]
def test_inspect_capability_saved_wrapper(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_inspect_capability_saved_wrapper(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_wrapper_inspect") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_wrapper_inspect")
artifact_store.save_artifact( artifact_store.save_artifact(
echo_artifact().model_copy(update={"id": "echo_wrapper", "kind": "wrapper"}) echo_artifact().model_copy(update={"id": "echo_wrapper", "kind": "wrapper"})
) )
api, _service = _capability_api(artifact_store) api, _service = _capability_api(artifact_store)
detail = asyncio.run( detail = await api.inspect_capability(qualified_name="workflow.echo_wrapper.v1")
api.inspect_capability(qualified_name="workflow.echo_wrapper.v1")
)
assert detail["name"] == "workflow.echo_wrapper.v1" assert detail["name"] == "workflow.echo_wrapper.v1"
assert detail["source_id"] == "workflow" assert detail["source_id"] == "workflow"
@@ -186,18 +189,17 @@ def test_inspect_capability_saved_wrapper(tmp_path: Path) -> None:
assert hints["output_map"] == {"echoed": "state.echoed"} assert hints["output_map"] == {"echoed": "state.echoed"}
def test_call_capability_saved_wrapper(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_call_capability_saved_wrapper(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_wrapper_call") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_wrapper_call")
artifact_store.save_artifact( artifact_store.save_artifact(
echo_artifact().model_copy(update={"id": "echo_wrapper", "kind": "wrapper"}) echo_artifact().model_copy(update={"id": "echo_wrapper", "kind": "wrapper"})
) )
api, service = _capability_api(artifact_store, register_echo=True) api, service = _capability_api(artifact_store, register_echo=True)
result = asyncio.run( result = await api.call_capability(
api.call_capability( qualified_name="workflow.echo_wrapper.v1",
qualified_name="workflow.echo_wrapper.v1", payload={"text": "hi"},
payload={"text": "hi"},
)
) )
assert result["kind"] == "wrapper_artifact" assert result["kind"] == "wrapper_artifact"
@@ -205,15 +207,14 @@ def test_call_capability_saved_wrapper(tmp_path: Path) -> None:
assert result["diagnostics"] == [] assert result["diagnostics"] == []
def test_create_draft_workspace_from_capability(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_create_draft_workspace_from_capability(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_draft_bootstrap") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_draft_bootstrap")
api, _service = _capability_api(artifact_store, register_echo=True) api, _service = _capability_api(artifact_store, register_echo=True)
result = asyncio.run( result = await api.create_draft_workspace_from_capability(
api.create_draft_workspace_from_capability( workspace_id="echo_ws",
workspace_id="echo_ws", capability_name="demo.personal.echo_tool",
capability_name="demo.personal.echo_tool",
)
) )
assert result["workspace_id"] == "echo_ws" assert result["workspace_id"] == "echo_ws"
@@ -222,13 +223,15 @@ def test_create_draft_workspace_from_capability(tmp_path: Path) -> None:
assert "next_actions" in result assert "next_actions" in result
assert result["wrapper_hints"]["capability_name"] == "demo.personal.echo_tool" assert result["wrapper_hints"]["capability_name"] == "demo.personal.echo_tool"
fetched = asyncio.run( fetched = await api.drafts.get_draft_workspace(
api.drafts.get_draft_workspace(workspace_id="echo_ws", include_draft=True) workspace_id="echo_ws", include_draft=True
) )
assert fetched["draft"]["steps"]["call"]["use"] == "demo.personal.echo_tool" assert fetched["draft"]["steps"]["call"]["use"] == "demo.personal.echo_tool"
def test_handler_delegates_to_capability_api(tmp_path: Path) -> None: @pytest.mark.asyncio
async def test_handler_delegates_to_capability_api(tmp_path: Path) -> None:
"""WorkflowSurfaceHandlers methods produce the same result as direct API.""" """WorkflowSurfaceHandlers methods produce the same result as direct API."""
artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_delegation") artifact_store = FileWorkflowArtifactStore(tmp_path / "cap_api_delegation")
mcp_root = artifact_store.root / "delegation_mcp" mcp_root = artifact_store.root / "delegation_mcp"
@@ -246,12 +249,10 @@ def test_handler_delegates_to_capability_api(tmp_path: Path) -> None:
context = context_from_service(service) context = context_from_service(service)
api = WorkflowCapabilityApi(context) api = WorkflowCapabilityApi(context)
handler_result = asyncio.run( handler_result = await h.inspect_capability(
h.inspect_capability(qualified_name="demo.personal.echo_tool") qualified_name="demo.personal.echo_tool"
)
api_result = asyncio.run(
api.inspect_capability(qualified_name="demo.personal.echo_tool")
) )
api_result = await api.inspect_capability(qualified_name="demo.personal.echo_tool")
assert handler_result["name"] == api_result["name"] assert handler_result["name"] == api_result["name"]
assert handler_result["wrapper_hints"] == api_result["wrapper_hints"] assert handler_result["wrapper_hints"] == api_result["wrapper_hints"]
+13 -13
View File
@@ -8,7 +8,7 @@ import pytest
from typer import Context as TyperContext from typer import Context as TyperContext
from typer.testing import CliRunner from typer.testing import CliRunner
from tests.wf_mcp.test_support import echo_tool, local_temp_root from tests.wf_mcp.test_support import echo_tool
from tests.wf_mcp.workflow_surface.conftest import echo_artifact from tests.wf_mcp.workflow_surface.conftest import echo_artifact
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
from wf_cli.app import app from wf_cli.app import app
@@ -144,8 +144,8 @@ def _write_cli_config(root: Path) -> Path:
return config_path return config_path
def test_wf_cap_list_outputs_json() -> None: def test_wf_cap_list_outputs_json(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_cap_list" root = tmp_path / "wf_cli_cap_list"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _write_cli_config(root) config_path = _write_cli_config(root)
@@ -163,8 +163,8 @@ def test_wf_cap_list_outputs_json() -> None:
assert payload["capabilities"][0]["source_id"] == "demo.personal" assert payload["capabilities"][0]["source_id"] == "demo.personal"
def test_wf_cap_list_ids_format() -> None: def test_wf_cap_list_ids_format(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_cap_list_ids" root = tmp_path / "wf_cli_cap_list_ids"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _write_cli_config(root) config_path = _write_cli_config(root)
@@ -189,8 +189,8 @@ def test_wf_cap_list_ids_format() -> None:
assert result.output.strip() == "demo.personal.echo_tool" assert result.output.strip() == "demo.personal.echo_tool"
def test_wf_cap_inspect_outputs_detail() -> None: def test_wf_cap_inspect_outputs_detail(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_cap_inspect" root = tmp_path / "wf_cli_cap_inspect"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _write_cli_config(root) config_path = _write_cli_config(root)
@@ -229,8 +229,8 @@ def _seed_echo_deployment(root: Path) -> Path:
return config_path return config_path
def test_wf_artifact_list_and_inspect() -> None: def test_wf_artifact_list_and_inspect(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_artifacts" root = tmp_path / "wf_cli_artifacts"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_artifact(root) config_path = _seed_echo_artifact(root)
@@ -249,8 +249,8 @@ def test_wf_artifact_list_and_inspect() -> None:
assert payload["version"] == 1 assert payload["version"] == 1
def test_wf_deploy_list_inspect_save_delete() -> None: def test_wf_deploy_list_inspect_save_delete(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_deploy_lifecycle" root = tmp_path / "wf_cli_deploy_lifecycle"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
@@ -290,8 +290,8 @@ def test_wf_deploy_list_inspect_save_delete() -> None:
assert json.loads(deleted.output)["deleted"] is True assert json.loads(deleted.output)["deleted"] is True
def test_wf_draft_create_patch_validate_save() -> None: def test_wf_draft_create_patch_validate_save(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_draft_lifecycle" root = tmp_path / "wf_cli_draft_lifecycle"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _write_cli_config(root) config_path = _write_cli_config(root)
+13 -13
View File
@@ -8,7 +8,7 @@ from unittest.mock import patch
from typer import Context as TyperContext from typer import Context as TyperContext
from typer.testing import CliRunner from typer.testing import CliRunner
from tests.wf_mcp.test_support import echo_tool, input_binding, local_temp_root from tests.wf_mcp.test_support import echo_tool, input_binding
from tests.wf_mcp.workflow_surface.conftest import echo_artifact from tests.wf_mcp.workflow_surface.conftest import echo_artifact
from wf_artifacts import FileWorkflowArtifactStore, WorkflowArtifact, WorkflowDeployment from wf_artifacts import FileWorkflowArtifactStore, WorkflowArtifact, WorkflowDeployment
from wf_cli.app import app from wf_cli.app import app
@@ -93,8 +93,8 @@ def _seed_interrupt_deployment(root: Path) -> Path:
return config_path return config_path
def test_wf_deploy_validate_outputs_json() -> None: def test_wf_deploy_validate_outputs_json(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_deploy_validate" root = tmp_path / "wf_cli_deploy_validate"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
@@ -115,8 +115,8 @@ def test_wf_deploy_validate_outputs_json() -> None:
) )
def test_wf_run_start_accepts_inline_json_input() -> None: def test_wf_run_start_accepts_inline_json_input(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_run_start" root = tmp_path / "wf_cli_run_start"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
@@ -144,8 +144,8 @@ def test_wf_run_start_accepts_inline_json_input() -> None:
assert payload["next_actions"]["can_continue"] is False assert payload["next_actions"]["can_continue"] is False
def test_wf_run_start_accepts_input_file() -> None: def test_wf_run_start_accepts_input_file(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_run_start_file" root = tmp_path / "wf_cli_run_start_file"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
input_path = root / "input.json" input_path = root / "input.json"
@@ -173,8 +173,8 @@ def test_wf_run_start_accepts_input_file() -> None:
assert payload["output"]["echoed"] == "from file" assert payload["output"]["echoed"] == "from file"
def test_wf_run_inspect_and_trace_existing_run() -> None: def test_wf_run_inspect_and_trace_existing_run(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_run_inspect_trace" root = tmp_path / "wf_cli_run_inspect_trace"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
@@ -389,8 +389,8 @@ def test_wf_run_watch_times_out_for_unstopped_run() -> None:
assert "did not stop before timeout" in result.output assert "did not stop before timeout" in result.output
def test_wf_run_resume_interrupted_run() -> None: def test_wf_run_resume_interrupted_run(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_run_resume" root = tmp_path / "wf_cli_run_resume"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_interrupt_deployment(root) config_path = _seed_interrupt_deployment(root)
@@ -432,8 +432,8 @@ def test_wf_run_resume_interrupted_run() -> None:
assert payload["resume_readiness"] == "not_applicable" assert payload["resume_readiness"] == "not_applicable"
def test_wf_run_start_reports_bad_json() -> None: def test_wf_run_start_reports_bad_json(tmp_path: Path) -> None:
root = local_temp_root() / "wf_cli_run_bad_json" root = tmp_path / "wf_cli_run_bad_json"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
+3 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from pathlib import Path
from typing import Any from typing import Any
from wf_mcp.models import BrokerConfig, ConnectionConfig from wf_mcp.models import BrokerConfig, ConnectionConfig
@@ -14,9 +15,9 @@ def structured(result: Any) -> dict[str, Any]:
return content return content
def proxy_config() -> BrokerConfig: def proxy_config(tmp_path: Path = local_temp_root()) -> BrokerConfig:
return BrokerConfig( return BrokerConfig(
store_root=local_temp_root() / "proxy_store", store_root=tmp_path / "proxy_store",
connections=[ connections=[
ConnectionConfig( ConnectionConfig(
id="fixture.personal", id="fixture.personal",
+10 -9
View File
@@ -3,18 +3,19 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import sys import sys
from pathlib import Path
import mcp.types as mcp_types import mcp.types as mcp_types
from wf_mcp.broker import load_broker_config from wf_mcp.broker import load_broker_config
from wf_mcp.proxy import create_proxy_client from wf_mcp.proxy import create_proxy_client
from ..test_support import fixture_server_path, local_temp_root from ..test_support import fixture_server_path
from .conftest import structured from .conftest import structured
def test_proxy_admin_tools_mutate_config_file() -> None: def test_proxy_admin_tools_mutate_config_file(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "proxy_admin_store" tmp_path = tmp_path / "proxy_admin_store"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text( config_path.write_text(
@@ -89,8 +90,8 @@ def test_proxy_admin_tools_mutate_config_file() -> None:
] ]
def test_proxy_admin_reload_remounts_connections() -> None: def test_proxy_admin_reload_remounts_connections(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "proxy_reload_store" tmp_path = tmp_path / "proxy_reload_store"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text( config_path.write_text(
@@ -130,8 +131,8 @@ def test_proxy_admin_reload_remounts_connections() -> None:
asyncio.run(run_proxy()) asyncio.run(run_proxy())
def test_proxy_admin_reload_sends_list_changed_notifications() -> None: def test_proxy_admin_reload_sends_list_changed_notifications(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "proxy_reload_notification_store" tmp_path = tmp_path / "proxy_reload_notification_store"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text( config_path.write_text(
@@ -164,8 +165,8 @@ def test_proxy_admin_reload_sends_list_changed_notifications() -> None:
assert "notifications/prompts/list_changed" in methods assert "notifications/prompts/list_changed" in methods
def test_proxy_config_mutation_does_not_notify_before_reload() -> None: def test_proxy_config_mutation_does_not_notify_before_reload(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "proxy_staged_notification_store" tmp_path = tmp_path / "proxy_staged_notification_store"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text( config_path.write_text(
+172 -197
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import sys import sys
from pathlib import Path
from typing import Any from typing import Any
import anyio import anyio
@@ -14,72 +15,69 @@ from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.proxy import create_proxy_client from wf_mcp.proxy import create_proxy_client
from wf_mcp.proxy.mounts import _bounded_proxy_list from wf_mcp.proxy.mounts import _bounded_proxy_list
from ..test_support import fixture_server_path, local_temp_root from ..test_support import fixture_server_path
from .conftest import proxy_config, structured from .conftest import proxy_config, structured
def test_proxy_lists_and_calls_upstream_tools() -> None: async def test_proxy_lists_and_calls_upstream_tools(tmp_path: Path) -> None:
config = proxy_config() config = proxy_config(tmp_path)
async def run_proxy() -> None: client = create_proxy_client(config)
client = create_proxy_client(config) async with client:
async with client: tools = await client.list_tools()
tools = await client.list_tools() names = [tool.name for tool in tools]
names = [tool.name for tool in tools] assert "wf.admin.list_connections" in names
assert "wf.admin.list_connections" in names assert "wf.admin.get_connection_statuses" in names
assert "wf.admin.get_connection_statuses" in names assert "wf.admin.list_proxy_tools" in names
assert "wf.admin.list_proxy_tools" in names assert "wf.admin.get_proxy_tool" in names
assert "wf.admin.get_proxy_tool" in names assert "fixture.personal.echo_tool" in names
assert "fixture.personal.echo_tool" in names
connections_result = await client.call_tool("wf.admin.list_connections") connections_result = await client.call_tool("wf.admin.list_connections")
connections = structured(connections_result)["result"] connections = structured(connections_result)["result"]
assert len(connections) == 1 assert len(connections) == 1
connection = connections[0] connection = connections[0]
assert connection["id"] == "fixture.personal" assert connection["id"] == "fixture.personal"
assert connection["server"] == "fixture" assert connection["server"] == "fixture"
assert connection["account"] == "personal" assert connection["account"] == "personal"
assert connection["enabled"] is True assert connection["enabled"] is True
assert connection["source_config_ownership"] == "locked" assert connection["source_config_ownership"] == "locked"
assert connection["metadata"] == { assert connection["metadata"] == {
"transport": "stdio", "transport": "stdio",
"command": sys.executable, "command": sys.executable,
"args": [fixture_server_path()], "args": [fixture_server_path()],
} }
result = await client.call_tool( result = await client.call_tool(
"fixture.personal.echo_tool", "fixture.personal.echo_tool",
{"text": "hello"}, {"text": "hello"},
) )
assert structured(result) == {"echoed": "hello"} assert structured(result) == {"echoed": "hello"}
proxy_tools_result = await client.call_tool("wf.admin.list_proxy_tools") proxy_tools_result = await client.call_tool("wf.admin.list_proxy_tools")
proxy_tools_payload = structured(proxy_tools_result) proxy_tools_payload = structured(proxy_tools_result)
proxy_tools = proxy_tools_payload["tools"] proxy_tools = proxy_tools_payload["tools"]
assert proxy_tools_payload["nextCursor"] is None assert proxy_tools_payload["nextCursor"] is None
assert proxy_tools_payload["total"] == 5 assert proxy_tools_payload["total"] == 5
assert len(proxy_tools) == 5 assert len(proxy_tools) == 5
assert proxy_tools[0]["proxy_name"] == "fixture.personal.echo_tool" assert proxy_tools[0]["proxy_name"] == "fixture.personal.echo_tool"
assert proxy_tools[0]["connection_id"] == "fixture.personal" assert proxy_tools[0]["connection_id"] == "fixture.personal"
assert proxy_tools[0]["local_name"] == "echo_tool" assert proxy_tools[0]["local_name"] == "echo_tool"
assert proxy_tools[0]["enabled"] is True assert proxy_tools[0]["enabled"] is True
proxy_names = [tool["proxy_name"] for tool in proxy_tools] proxy_names = [tool["proxy_name"] for tool in proxy_tools]
assert "fixture.personal.emit_notifications_tool" in proxy_names assert "fixture.personal.emit_notifications_tool" in proxy_names
assert "fixture.personal.remember_value_tool" in proxy_names assert "fixture.personal.remember_value_tool" in proxy_names
assert "fixture.personal.recall_value_tool" in proxy_names assert "fixture.personal.recall_value_tool" in proxy_names
assert "fixture.personal.resource_link_tool" in proxy_names assert "fixture.personal.resource_link_tool" in proxy_names
proxy_tool_result = await client.call_tool( proxy_tool_result = await client.call_tool(
"wf.admin.get_proxy_tool", "wf.admin.get_proxy_tool",
{"proxy_name": "fixture.personal.echo_tool"}, {"proxy_name": "fixture.personal.echo_tool"},
) )
proxy_tool = structured(proxy_tool_result) proxy_tool = structured(proxy_tool_result)
assert proxy_tool["proxy_name"] == "fixture.personal.echo_tool" assert proxy_tool["proxy_name"] == "fixture.personal.echo_tool"
assert proxy_tool["connection_id"] == "fixture.personal" assert proxy_tool["connection_id"] == "fixture.personal"
assert proxy_tool["local_name"] == "echo_tool" assert proxy_tool["local_name"] == "echo_tool"
assert proxy_tool["input_schema"]["properties"]["text"]["type"] == "string" assert proxy_tool["input_schema"]["properties"]["text"]["type"] == "string"
asyncio.run(run_proxy())
def test_proxy_listing_degrades_when_one_source_hangs() -> None: def test_proxy_listing_degrades_when_one_source_hangs() -> None:
@@ -183,60 +181,49 @@ def test_proxy_listing_degrades_when_session_transport_closes(
assert "remote.default" in caplog.text assert "remote.default" in caplog.text
def test_proxy_registers_admin_tools_on_local_provider() -> None: async def test_proxy_registers_admin_tools_on_local_provider(tmp_path) -> None:
config = proxy_config() config = proxy_config(tmp_path)
async def run_proxy() -> None: client = create_proxy_client(config)
client = create_proxy_client(config) async with client:
async with client: tools = await client.list_tools()
tools = await client.list_tools() admin_names = [tool.name for tool in tools if tool.name.startswith("wf.admin.")]
admin_names = [ assert "wf.admin.list_connections" in admin_names
tool.name for tool in tools if tool.name.startswith("wf.admin.") assert "wf.admin.get_connection_statuses" in admin_names
] assert "wf.admin.list_proxy_tools" in admin_names
assert "wf.admin.list_connections" in admin_names assert "wf.admin.get_proxy_tool" in admin_names
assert "wf.admin.get_connection_statuses" in admin_names
assert "wf.admin.list_proxy_tools" in admin_names
assert "wf.admin.get_proxy_tool" in admin_names
asyncio.run(run_proxy())
def test_proxy_rewrites_resource_links_returned_by_tools() -> None: async def test_proxy_rewrites_resource_links_returned_by_tools(tmp_path) -> None:
config = proxy_config() config = proxy_config(tmp_path)
async def run_proxy() -> None: client = create_proxy_client(config)
client = create_proxy_client(config) async with client:
async with client: result = await client.call_tool("fixture.personal.resource_link_tool")
result = await client.call_tool("fixture.personal.resource_link_tool") link = result.content[0]
link = result.content[0] assert link.type == "resource_link"
assert link.type == "resource_link" assert str(link.uri) == "fixture://fixture/personal/docs/welcome"
assert str(link.uri) == "fixture://fixture/personal/docs/welcome"
asyncio.run(run_proxy())
def test_proxy_reuses_one_upstream_session_for_stateful_tools() -> None: async def test_proxy_reuses_one_upstream_session_for_stateful_tools(tmp_path) -> None:
"""Visible proxy tools must share server-local state for one MCP client.""" """Visible proxy tools must share server-local state for one MCP client."""
config = proxy_config() config = proxy_config(tmp_path)
async def run_proxy() -> None: client = create_proxy_client(config)
client = create_proxy_client(config) async with client:
async with client: written = await client.call_tool(
written = await client.call_tool( "fixture.personal.remember_value_tool",
"fixture.personal.remember_value_tool", {"value": "held"},
{"value": "held"}, )
) recalled = await client.call_tool("fixture.personal.recall_value_tool")
recalled = await client.call_tool("fixture.personal.recall_value_tool")
assert structured(written)["remembered"] == "held" assert structured(written)["remembered"] == "held"
assert structured(recalled)["remembered"] == "held" assert structured(recalled)["remembered"] == "held"
asyncio.run(run_proxy())
def test_proxy_rejects_invalid_connection_config() -> None: def test_proxy_rejects_invalid_connection_config(tmp_path) -> None:
config = BrokerConfig( config = BrokerConfig(
store_root=local_temp_root() / "proxy_invalid_store", store_root=tmp_path / "proxy_invalid_store",
connections=[ connections=[
ConnectionConfig( ConnectionConfig(
id="fixture.personal", id="fixture.personal",
@@ -291,103 +278,91 @@ def test_proxy_rejects_invalid_connection_config() -> None:
assert "connection id 'wf.admin' is reserved by wf-mcp" in message assert "connection id 'wf.admin' is reserved by wf-mcp" in message
def test_proxy_can_expose_resources_and_prompts_as_tools() -> None: async def test_proxy_can_expose_resources_and_prompts_as_tools(tmp_path) -> None:
config = proxy_config() config = proxy_config(tmp_path)
async def run_proxy() -> None: client = create_proxy_client(
client = create_proxy_client( config,
config, resources_as_tools=True,
resources_as_tools=True, prompts_as_tools=True,
prompts_as_tools=True, )
async with client:
tools = await client.list_tools()
names = [tool.name for tool in tools]
assert "list_resources" in names
assert "read_resource" in names
assert "list_prompts" in names
assert "get_prompt" in names
async def test_proxy_can_collapse_upstream_tools_behind_search(tmp_path) -> None:
config = BrokerConfig(
store_root=tmp_path / "search_proxy_store",
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
client = create_proxy_client(config, search_tools=True)
async with client:
tools = await client.list_tools()
names = [tool.name for tool in tools]
assert "search_tools" in names
assert "wf.admin.list_connections" in names
assert "wf.admin.get_connection_statuses" in names
assert "wf.admin.list_proxy_tools" in names
assert "fixture.personal.echo_tool" not in names
async def test_proxy_admin_inventory_ignores_search_visibility(tmp_path) -> None:
config = BrokerConfig(
store_root=tmp_path / "search_admin_store",
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
client = create_proxy_client(config, search_tools=True)
async with client:
result = await client.call_tool("wf.admin.list_proxy_tools")
payload = structured(result)
assert payload["total"] > 0
async def test_proxy_proxy_tool_listing_supports_filters_and_cursor(tmp_path) -> None:
config = proxy_config(tmp_path)
client = create_proxy_client(config)
async with client:
result = await client.call_tool(
"wf.admin.list_proxy_tools",
{"limit": 2},
) )
async with client: payload = structured(result)
tools = await client.list_tools() assert len(payload["tools"]) == 2
names = [tool.name for tool in tools] assert payload["nextCursor"] is not None
assert "list_resources" in names
assert "read_resource" in names
assert "list_prompts" in names
assert "get_prompt" in names
asyncio.run(run_proxy()) result2 = await client.call_tool(
"wf.admin.list_proxy_tools",
{"limit": 2, "cursor": payload["nextCursor"]},
def test_proxy_can_collapse_upstream_tools_behind_search() -> None: )
config = BrokerConfig( payload2 = structured(result2)
store_root=local_temp_root() / "search_proxy_store", assert len(payload2["tools"]) > 0
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
async def run_proxy() -> None:
client = create_proxy_client(config, search_tools=True)
async with client:
tools = await client.list_tools()
names = [tool.name for tool in tools]
assert "search_tools" in names
assert "wf.admin.list_connections" in names
assert "wf.admin.get_connection_statuses" in names
assert "wf.admin.list_proxy_tools" in names
assert "fixture.personal.echo_tool" not in names
asyncio.run(run_proxy())
def test_proxy_admin_inventory_ignores_search_visibility() -> None:
config = BrokerConfig(
store_root=local_temp_root() / "search_admin_store",
connections=[
ConnectionConfig(
id="fixture.personal",
server="fixture",
account="personal",
metadata={
"transport": "stdio",
"command": sys.executable,
"args": [fixture_server_path()],
},
)
],
)
async def run_proxy() -> None:
client = create_proxy_client(config, search_tools=True)
async with client:
result = await client.call_tool("wf.admin.list_proxy_tools")
payload = structured(result)
assert payload["total"] > 0
asyncio.run(run_proxy())
def test_proxy_proxy_tool_listing_supports_filters_and_cursor() -> None:
config = proxy_config()
async def run_proxy() -> None:
client = create_proxy_client(config)
async with client:
result = await client.call_tool(
"wf.admin.list_proxy_tools",
{"limit": 2},
)
payload = structured(result)
assert len(payload["tools"]) == 2
assert payload["nextCursor"] is not None
result2 = await client.call_tool(
"wf.admin.list_proxy_tools",
{"limit": 2, "cursor": payload["nextCursor"]},
)
payload2 = structured(result2)
assert len(payload2["tools"]) > 0
asyncio.run(run_proxy())
+62 -47
View File
@@ -20,13 +20,12 @@ from ..test_support import (
FakeAdapter, FakeAdapter,
echo_tool, echo_tool,
finalize_tool, finalize_tool,
local_temp_root,
) )
from .conftest import single_echo_plan from .conftest import single_echo_plan
def test_service_builds_namespaced_catalog() -> None: def test_service_builds_namespaced_catalog(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "catalog_store")) service = WfMcpService(store=FileStore(tmp_path / "catalog_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
@@ -41,8 +40,8 @@ def test_service_builds_namespaced_catalog() -> None:
] ]
def test_service_rejects_reserved_connection_ids() -> None: def test_service_rejects_reserved_connection_ids(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "reserved_ids_store")) service = WfMcpService(store=FileStore(tmp_path / "reserved_ids_store"))
for connection_id in ("wf.admin", "wf.mcp"): for connection_id in ("wf.admin", "wf.mcp"):
try: try:
@@ -56,8 +55,8 @@ def test_service_rejects_reserved_connection_ids() -> None:
raise AssertionError(f"expected {connection_id!r} to be rejected") raise AssertionError(f"expected {connection_id!r} to be rejected")
def test_service_installs_builtin_stdlib_specs_by_default() -> None: def test_service_installs_builtin_stdlib_specs_by_default(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "builtin_store")) service = WfMcpService(store=FileStore(tmp_path / "builtin_store"))
assert ( assert (
"wf.std.runtime_error" "wf.std.runtime_error"
@@ -66,8 +65,8 @@ def test_service_installs_builtin_stdlib_specs_by_default() -> None:
assert "wf.mcp" not in service.capability_sources assert "wf.mcp" not in service.capability_sources
def test_service_does_not_install_workflow_stores_implicitly() -> None: def test_service_does_not_install_workflow_stores_implicitly(tmp_path: Path) -> None:
root = local_temp_root() / "service_no_implicit_workflow_stores" root = tmp_path / "service_no_implicit_workflow_stores"
service = WfMcpService(store=FileStore(root)) service = WfMcpService(store=FileStore(root))
assert service.artifact_store is None assert service.artifact_store is None
@@ -75,8 +74,10 @@ def test_service_does_not_install_workflow_stores_implicitly() -> None:
assert service.run_store is None assert service.run_store is None
def test_service_registers_empty_source_for_connection_without_catalog() -> None: def test_service_registers_empty_source_for_connection_without_catalog(
service = WfMcpService(store=FileStore(local_temp_root() / "empty_source")) tmp_path: Path,
) -> None:
service = WfMcpService(store=FileStore(tmp_path / "empty_source"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -88,8 +89,10 @@ def test_service_registers_empty_source_for_connection_without_catalog() -> None
assert source.description == "No catalog loaded for demo.personal." assert source.description == "No catalog loaded for demo.personal."
def test_service_lists_all_capability_sources_with_owned_capability_names() -> None: def test_service_lists_all_capability_sources_with_owned_capability_names(
service = WfMcpService(store=FileStore(local_temp_root() / "source_inventory")) tmp_path: Path,
) -> None:
service = WfMcpService(store=FileStore(tmp_path / "source_inventory"))
sources = service.list_sources() sources = service.list_sources()
sources_by_id = {source["id"]: source for source in sources} sources_by_id = {source["id"]: source for source in sources}
@@ -112,8 +115,8 @@ def test_service_lists_all_capability_sources_with_owned_capability_names() -> N
assert "wf.admin.list_sources" in admin_source["capabilities"]["tools"] assert "wf.admin.list_sources" in admin_source["capabilities"]["tools"]
def test_service_lists_compact_source_summaries() -> None: def test_service_lists_compact_source_summaries(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_summaries")) service = WfMcpService(store=FileStore(tmp_path / "source_summaries"))
payload = service.list_source_summaries(limit=1) payload = service.list_source_summaries(limit=1)
@@ -129,8 +132,8 @@ def test_service_lists_compact_source_summaries() -> None:
assert std_source["has_more"]["node_specs"] is True assert std_source["has_more"]["node_specs"] is True
def test_wf_std_source_contains_authoring_ops() -> None: def test_wf_std_source_contains_authoring_ops(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "stdlib_source_store")) service = WfMcpService(store=FileStore(tmp_path / "stdlib_source_store"))
specs = service.capability_sources["wf.std"].capabilities.node_specs specs = service.capability_sources["wf.std"].capabilities.node_specs
expected = { expected = {
@@ -158,8 +161,8 @@ def test_wf_std_source_contains_authoring_ops() -> None:
assert set(specs) == expected assert set(specs) == expected
def test_wf_std_source_contains_builtin_reducers() -> None: def test_wf_std_source_contains_builtin_reducers(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "stdlib_reducer_store")) service = WfMcpService(store=FileStore(tmp_path / "stdlib_reducer_store"))
reducers = service.capability_sources["wf.std"].capabilities.reducers reducers = service.capability_sources["wf.std"].capabilities.reducers
assert set(reducers) == { assert set(reducers) == {
@@ -172,8 +175,8 @@ def test_wf_std_source_contains_builtin_reducers() -> None:
} }
def test_service_sources_have_visibility_and_capability_buckets() -> None: def test_service_sources_have_visibility_and_capability_buckets(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_shape_store")) service = WfMcpService(store=FileStore(tmp_path / "source_shape_store"))
std_source = service.capability_sources["wf.std"] std_source = service.capability_sources["wf.std"]
@@ -186,8 +189,8 @@ def test_service_sources_have_visibility_and_capability_buckets() -> None:
assert not std_source.capabilities.tools assert not std_source.capabilities.tools
def test_wf_recipes_source_contains_composed_capabilities() -> None: def test_wf_recipes_source_contains_composed_capabilities(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "recipes_source_store")) service = WfMcpService(store=FileStore(tmp_path / "recipes_source_store"))
specs = service.capability_sources["wf.recipes"].capabilities.node_specs specs = service.capability_sources["wf.recipes"].capabilities.node_specs
assert set(specs) == {"wf.recipes.extract_text_content"} assert set(specs) == {"wf.recipes.extract_text_content"}
@@ -196,8 +199,8 @@ def test_wf_recipes_source_contains_composed_capabilities() -> None:
) )
def test_wf_admin_source_exists_but_is_not_planner_visible() -> None: def test_wf_admin_source_exists_but_is_not_planner_visible(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "admin_source_store")) service = WfMcpService(store=FileStore(tmp_path / "admin_source_store"))
source = service.capability_sources["wf.admin"] source = service.capability_sources["wf.admin"]
assert source.kind == "system" assert source.kind == "system"
@@ -214,9 +217,9 @@ def test_wf_admin_source_exists_but_is_not_planner_visible() -> None:
assert "wf.admin" not in service.get_planner_catalog().snapshots assert "wf.admin" not in service.get_planner_catalog().snapshots
def test_service_can_disable_builtin_stdlib_specs() -> None: def test_service_can_disable_builtin_stdlib_specs(tmp_path: Path) -> None:
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "no_builtin_store"), store=FileStore(tmp_path / "no_builtin_store"),
include_builtin_specs=False, include_builtin_specs=False,
) )
@@ -224,8 +227,8 @@ def test_service_can_disable_builtin_stdlib_specs() -> None:
assert "wf.recipes" not in service.capability_sources assert "wf.recipes" not in service.capability_sources
def test_service_planner_catalog_excludes_hidden_sources() -> None: def test_service_planner_catalog_excludes_hidden_sources(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "hidden_list_store")) service = WfMcpService(store=FileStore(tmp_path / "hidden_list_store"))
hidden_echo_tool = NodeSpec( hidden_echo_tool = NodeSpec(
name="hidden.source.echo_tool", name="hidden.source.echo_tool",
input_model=echo_tool.input_model, input_model=echo_tool.input_model,
@@ -255,8 +258,10 @@ def test_service_planner_catalog_excludes_hidden_sources() -> None:
assert "hidden.source.echo_tool" not in planner_names assert "hidden.source.echo_tool" not in planner_names
def test_service_catalog_split_keeps_system_specs_out_of_backend_catalog() -> None: def test_service_catalog_split_keeps_system_specs_out_of_backend_catalog(
service = WfMcpService(store=FileStore(local_temp_root() / "planner_store")) tmp_path: Path,
) -> None:
service = WfMcpService(store=FileStore(tmp_path / "planner_store"))
backend_payload = service.get_catalog().as_payload() backend_payload = service.get_catalog().as_payload()
planner_payload = service.get_planner_catalog().as_payload() planner_payload = service.get_planner_catalog().as_payload()
@@ -268,8 +273,10 @@ 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
async def test_service_hydrates_planner_specs_from_stored_catalog() -> None: async def test_service_hydrates_planner_specs_from_stored_catalog(
store = local_temp_root() / "restart_planner_store" tmp_path: Path,
) -> None:
store = tmp_path / "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))
first_service.register_connection( first_service.register_connection(
@@ -298,8 +305,10 @@ async def test_service_hydrates_planner_specs_from_stored_catalog() -> None:
assert run.output["echoed"] == "hello" assert run.output["echoed"] == "hello"
def test_source_catalog_service_registers_and_lists_sources_directly() -> None: def test_source_catalog_service_registers_and_lists_sources_directly(
store = FileStore(local_temp_root() / "source_catalog_direct") tmp_path: Path,
) -> None:
store = FileStore(tmp_path / "source_catalog_direct")
def unused_tool_executor(connection: ConnectionConfig): def unused_tool_executor(connection: ConnectionConfig):
raise AssertionError("tool executor should not be used by source listing") raise AssertionError("tool executor should not be used by source listing")
@@ -333,19 +342,21 @@ def test_source_catalog_service_registers_and_lists_sources_directly() -> None:
assert payload["sources"][0]["id"] == "demo.personal" assert payload["sources"][0]["id"] == "demo.personal"
def test_wfmcpservice_capability_sources_proxy_source_catalog() -> None: def test_wfmcpservice_capability_sources_proxy_source_catalog(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "source_catalog_proxy")) service = WfMcpService(store=FileStore(tmp_path / "source_catalog_proxy"))
assert service.capability_sources is service.source_catalog.capability_sources assert service.capability_sources is service.source_catalog.capability_sources
assert "wf.std" in service.source_catalog.capability_sources assert "wf.std" in service.source_catalog.capability_sources
def test_source_catalog_service_excludes_hidden_sources_from_planner_catalog() -> None: def test_source_catalog_service_excludes_hidden_sources_from_planner_catalog(
tmp_path: Path,
) -> None:
def unused_tool_executor(connection: ConnectionConfig): def unused_tool_executor(connection: ConnectionConfig):
raise AssertionError("tool executor should not be used by planner listing") raise AssertionError("tool executor should not be used by planner listing")
catalog = SourceCatalogService( catalog = SourceCatalogService(
store=FileStore(local_temp_root() / "source_catalog_hidden"), store=FileStore(tmp_path / "source_catalog_hidden"),
connection_lookup=lambda connection_id: ConnectionConfig( connection_lookup=lambda connection_id: ConnectionConfig(
id=connection_id, id=connection_id,
server="demo", server="demo",
@@ -410,10 +421,10 @@ 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
async def test_source_catalog_hydrates_connection_source_from_snapshot_directly() -> ( async def test_source_catalog_hydrates_connection_source_from_snapshot_directly(
None tmp_path: Path,
): ) -> None:
root = local_temp_root() / "source_catalog_hydrate_direct" root = tmp_path / "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))
first_service.register_connection( first_service.register_connection(
@@ -434,7 +445,9 @@ async def test_source_catalog_hydrates_connection_source_from_snapshot_directly(
assert "demo.personal.echo_tool" in specs assert "demo.personal.echo_tool" in specs
def test_source_catalog_register_specs_replaces_discovered_specs_directly() -> None: def test_source_catalog_register_specs_replaces_discovered_specs_directly(
tmp_path: Path,
) -> None:
connection = ConnectionConfig( connection = ConnectionConfig(
id="demo.personal", id="demo.personal",
server="demo", server="demo",
@@ -445,7 +458,7 @@ def test_source_catalog_register_specs_replaces_discovered_specs_directly() -> N
raise AssertionError("tool executor should not be used by spec registration") raise AssertionError("tool executor should not be used by spec registration")
catalog = SourceCatalogService( catalog = SourceCatalogService(
store=FileStore(local_temp_root() / "source_catalog_register_specs"), store=FileStore(tmp_path / "source_catalog_register_specs"),
connection_lookup=lambda connection_id: connection, connection_lookup=lambda connection_id: connection,
connection_list_enabled=lambda: [connection], connection_list_enabled=lambda: [connection],
connection_list_all=lambda: [connection], connection_list_all=lambda: [connection],
@@ -476,8 +489,10 @@ def test_source_catalog_register_specs_replaces_discovered_specs_directly() -> N
assert catalog.store.load_catalog("demo.personal") is not None assert catalog.store.load_catalog("demo.personal") is not None
def test_source_catalog_finds_local_documentation_resource_directly() -> None: def test_source_catalog_finds_local_documentation_resource_directly(
service = WfMcpService(store=FileStore(local_temp_root() / "source_local_docs")) tmp_path: Path,
) -> None:
service = WfMcpService(store=FileStore(tmp_path / "source_local_docs"))
test_resource = DocumentationResource( test_resource = DocumentationResource(
name="test.docs.example", name="test.docs.example",
uri="wf://docs/example", uri="wf://docs/example",
+9 -10
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import Any from typing import Any
from wf_mcp.admin_surface import BrokerAdminHandlers, TransparentAdminHandlers from wf_mcp.admin_surface import BrokerAdminHandlers, TransparentAdminHandlers
@@ -8,11 +9,9 @@ from wf_mcp.broker import WfMcpService
from wf_mcp.models import BrokerConfig, ConnectionConfig from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.storage import FileStore from wf_mcp.storage import FileStore
from .test_support import local_temp_root
def test_broker_admin_handlers_list_connections_and_events(tmp_path: Path) -> None:
def test_broker_admin_handlers_list_connections_and_events() -> None: service = WfMcpService(store=FileStore(tmp_path / "admin_broker_store"))
service = WfMcpService(store=FileStore(local_temp_root() / "admin_broker_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
@@ -34,8 +33,8 @@ def test_broker_admin_handlers_list_connections_and_events() -> None:
assert sources["total"] >= 2 assert sources["total"] >= 2
def test_broker_admin_handlers_report_failed_refresh_payload() -> None: def test_broker_admin_handlers_report_failed_refresh_payload(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "admin_refresh_store")) service = WfMcpService(store=FileStore(tmp_path / "admin_refresh_store"))
handlers = BrokerAdminHandlers(service) handlers = BrokerAdminHandlers(service)
payload = _run(handlers.refresh_connection_catalog("missing.personal")) payload = _run(handlers.refresh_connection_catalog("missing.personal"))
@@ -45,8 +44,8 @@ def test_broker_admin_handlers_report_failed_refresh_payload() -> None:
assert payload["error_type"] == "KeyError" assert payload["error_type"] == "KeyError"
def test_transparent_admin_handlers_delegate_config_operations() -> None: def test_transparent_admin_handlers_delegate_config_operations(tmp_path: Path) -> None:
runtime = FakeProxyAdminRuntime() runtime = FakeProxyAdminRuntime(tmp_path)
handlers = TransparentAdminHandlers(runtime) handlers = TransparentAdminHandlers(runtime)
connections = handlers.list_connections() connections = handlers.list_connections()
@@ -130,10 +129,10 @@ class FakeManager:
class FakeProxyAdminRuntime: class FakeProxyAdminRuntime:
def __init__(self) -> None: def __init__(self, tmp_path: Path) -> None:
self.manager = FakeManager(added=[]) self.manager = FakeManager(added=[])
self._config = BrokerConfig( self._config = BrokerConfig(
store_root=local_temp_root() / "transparent_admin_handlers_store", store_root=tmp_path / "transparent_admin_handlers_store",
connections=[ connections=[
ConnectionConfig( ConnectionConfig(
id="demo.personal", id="demo.personal",
+57 -56
View File
@@ -33,13 +33,12 @@ from .test_support import (
FakeAdapter, FakeAdapter,
echo_tool, echo_tool,
input_binding, input_binding,
local_temp_root,
output_binding, output_binding,
) )
def test_load_broker_config_resolves_relative_store_root() -> None: def test_load_broker_config_resolves_relative_store_root(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "broker_config_test" tmp_path = tmp_path / "broker_config_test"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text( config_path.write_text(
@@ -64,8 +63,10 @@ def test_load_broker_config_resolves_relative_store_root() -> None:
assert [connection.id for connection in config.connections] == ["demo.personal"] assert [connection.id for connection in config.connections] == ["demo.personal"]
def test_create_broker_server_exposes_tools_resources_and_prompts() -> None: def test_create_broker_server_exposes_tools_resources_and_prompts(
service = WfMcpService(store=FileStore(local_temp_root() / "broker_server_store")) tmp_path: Path,
) -> None:
service = WfMcpService(store=FileStore(tmp_path / "broker_server_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
@@ -111,8 +112,8 @@ def test_create_broker_server_exposes_tools_resources_and_prompts() -> None:
assert "demo.personal" in all_source_ids assert "demo.personal" in all_source_ids
def test_broker_admin_tools_are_backed_by_wf_admin_source() -> None: def test_broker_admin_tools_are_backed_by_wf_admin_source(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "broker_admin_source")) service = WfMcpService(store=FileStore(tmp_path / "broker_admin_source"))
server = create_broker_server(service) server = create_broker_server(service)
tools = asyncio.run(server.list_tools()) tools = asyncio.run(server.list_tools())
@@ -125,9 +126,9 @@ def test_broker_admin_tools_are_backed_by_wf_admin_source() -> None:
) )
def test_build_service_from_config_registers_connections() -> None: def test_build_service_from_config_registers_connections(tmp_path: Path) -> None:
config = BrokerConfig( config = BrokerConfig(
store_root=local_temp_root() / "broker_config_store", store_root=tmp_path / "broker_config_store",
connections=[ connections=[
ConnectionConfig(id="demo.personal", server="demo", account="personal"), ConnectionConfig(id="demo.personal", server="demo", account="personal"),
ConnectionConfig(id="demo.work", server="demo", account="work"), ConnectionConfig(id="demo.work", server="demo", account="work"),
@@ -140,8 +141,8 @@ def test_build_service_from_config_registers_connections() -> None:
assert ids == ["demo.personal", "demo.work"] assert ids == ["demo.personal", "demo.work"]
def test_broker_refresh_tool_returns_structured_error() -> None: def test_broker_refresh_tool_returns_structured_error(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "broker_fail_store")) service = WfMcpService(store=FileStore(tmp_path / "broker_fail_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
@@ -162,11 +163,11 @@ def test_broker_refresh_tool_returns_structured_error() -> None:
} }
def test_broker_lists_workflow_artifacts_from_artifact_store() -> None: def test_broker_lists_workflow_artifacts_from_artifact_store(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "broker_artifacts") artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_artifacts")
artifact_store.save_artifact(_artifact()) artifact_store.save_artifact(_artifact())
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_artifacts_mcp_store"), store=FileStore(tmp_path / "broker_artifacts_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -181,13 +182,11 @@ def test_broker_lists_workflow_artifacts_from_artifact_store() -> None:
assert "plan" not in nodes[0] assert "plan" not in nodes[0]
def test_broker_inspects_workflow_artifact_from_artifact_store() -> None: def test_broker_inspects_workflow_artifact_from_artifact_store(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_inspect_artifacts")
local_temp_root() / "broker_inspect_artifacts"
)
artifact_store.save_artifact(_artifact()) artifact_store.save_artifact(_artifact())
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_inspect_mcp_store"), store=FileStore(tmp_path / "broker_inspect_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -205,10 +204,10 @@ def test_broker_inspects_workflow_artifact_from_artifact_store() -> None:
assert artifact["plan"]["name"] == "summarize_docs" assert artifact["plan"]["name"] == "summarize_docs"
def test_broker_validates_workflow_deployment_from_artifact_store() -> None: def test_broker_validates_workflow_deployment_from_artifact_store(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "broker_validate_artifacts" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_validate_artifacts")
artifact_store.save_artifact(_artifact()) artifact_store.save_artifact(_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
WorkflowDeployment( WorkflowDeployment(
@@ -221,7 +220,7 @@ def test_broker_validates_workflow_deployment_from_artifact_store() -> None:
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_validate_mcp_store"), store=FileStore(tmp_path / "broker_validate_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -240,12 +239,10 @@ def test_broker_validates_workflow_deployment_from_artifact_store() -> None:
assert payload["diagnostics"][0]["code"] == "source_missing" assert payload["diagnostics"][0]["code"] == "source_missing"
def test_broker_saves_workflow_artifact() -> None: def test_broker_saves_workflow_artifact(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_save_artifacts")
local_temp_root() / "broker_save_artifacts"
)
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_save_mcp_store"), store=FileStore(tmp_path / "broker_save_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -264,12 +261,12 @@ def test_broker_saves_workflow_artifact() -> None:
assert loaded.title == "Summarize Docs" assert loaded.title == "Summarize Docs"
def test_broker_creates_workflow_artifact_from_plan() -> None: def test_broker_creates_workflow_artifact_from_plan(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "broker_create_artifact_from_plan" tmp_path / "broker_create_artifact_from_plan"
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_create_artifact_mcp_store"), store=FileStore(tmp_path / "broker_create_artifact_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -306,12 +303,10 @@ def test_broker_creates_workflow_artifact_from_plan() -> None:
assert loaded.required_capability_map()["demo.echo_tool"].logical_source == "demo" assert loaded.required_capability_map()["demo.echo_tool"].logical_source == "demo"
def test_broker_saves_and_lists_workflow_deployments() -> None: def test_broker_saves_and_lists_workflow_deployments(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_save_deployments")
local_temp_root() / "broker_save_deployments"
)
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_save_deployments_mcp_store"), store=FileStore(tmp_path / "broker_save_deployments_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -345,10 +340,8 @@ def test_broker_saves_and_lists_workflow_deployments() -> None:
assert "bindings" not in list_payload["deployments"][0] assert "bindings" not in list_payload["deployments"][0]
def test_broker_runs_non_interrupting_workflow_deployment() -> None: def test_broker_runs_non_interrupting_workflow_deployment(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "broker_run_artifacts")
local_temp_root() / "broker_run_artifacts"
)
artifact_store.save_artifact(_echo_artifact()) artifact_store.save_artifact(_echo_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
WorkflowDeployment( WorkflowDeployment(
@@ -359,9 +352,9 @@ def test_broker_runs_non_interrupting_workflow_deployment() -> None:
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_run_mcp_store"), store=FileStore(tmp_path / "broker_run_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "broker_run_mcp_store"), run_store=FileRunStore(tmp_path / "broker_run_mcp_store"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -388,9 +381,11 @@ def test_broker_runs_non_interrupting_workflow_deployment() -> None:
assert payload["trace_count"] > 0 assert payload["trace_count"] > 0
def test_broker_run_deployment_returns_unrunnable_for_dependency_errors() -> None: def test_broker_run_deployment_returns_unrunnable_for_dependency_errors(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "broker_run_unrunnable_artifacts" tmp_path / "broker_run_unrunnable_artifacts"
) )
artifact_store.save_artifact(_artifact()) artifact_store.save_artifact(_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
@@ -404,7 +399,7 @@ def test_broker_run_deployment_returns_unrunnable_for_dependency_errors() -> Non
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_run_unrunnable_mcp_store"), store=FileStore(tmp_path / "broker_run_unrunnable_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -425,9 +420,11 @@ def test_broker_run_deployment_returns_unrunnable_for_dependency_errors() -> Non
assert payload["diagnostics"][0]["code"] == "source_missing" assert payload["diagnostics"][0]["code"] == "source_missing"
def test_broker_run_deployment_pauses_and_resumes_interrupting_artifacts() -> None: def test_broker_run_deployment_pauses_and_resumes_interrupting_artifacts(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "broker_run_interrupt_artifacts" tmp_path / "broker_run_interrupt_artifacts"
) )
artifact_store.save_artifact(_interrupt_artifact()) artifact_store.save_artifact(_interrupt_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
@@ -439,9 +436,9 @@ def test_broker_run_deployment_pauses_and_resumes_interrupting_artifacts() -> No
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "broker_run_interrupt_mcp_store"), store=FileStore(tmp_path / "broker_run_interrupt_mcp_store"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "broker_run_interrupt_mcp_store"), run_store=FileRunStore(tmp_path / "broker_run_interrupt_mcp_store"),
) )
server = create_broker_server(service) server = create_broker_server(service)
@@ -478,8 +475,10 @@ def test_broker_run_deployment_pauses_and_resumes_interrupting_artifacts() -> No
assert resumed["resume_readiness"] == "not_applicable" assert resumed["resume_readiness"] == "not_applicable"
def test_build_service_from_config_uses_store_root_for_workflow_stores() -> None: def test_build_service_from_config_uses_store_root_for_workflow_stores(
store_root = local_temp_root() / "broker_config_workflow_stores" tmp_path: Path,
) -> None:
store_root = tmp_path / "broker_config_workflow_stores"
config = BrokerConfig(store_root=store_root, connections=[]) config = BrokerConfig(store_root=store_root, connections=[])
service = build_service_from_config(config) service = build_service_from_config(config)
@@ -512,8 +511,10 @@ def _registry_entry(
) )
def test_build_service_from_config_loads_source_registry_entries() -> None: def test_build_service_from_config_loads_source_registry_entries(
tmp_path = local_temp_root() / "broker_config_registry_load" tmp_path: Path,
) -> None:
tmp_path = tmp_path / "broker_config_registry_load"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config = BrokerConfig(store_root=tmp_path, connections=[]) config = BrokerConfig(store_root=tmp_path, connections=[])
FileSourceRegistryStore(tmp_path).save_registry( FileSourceRegistryStore(tmp_path).save_registry(
@@ -527,8 +528,8 @@ def test_build_service_from_config_loads_source_registry_entries() -> None:
assert "fixture.registry" in service.capability_sources assert "fixture.registry" in service.capability_sources
def test_build_service_from_config_config_shadows_registry() -> None: def test_build_service_from_config_config_shadows_registry(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "broker_config_registry_shadow" tmp_path = tmp_path / "broker_config_registry_shadow"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config = BrokerConfig( config = BrokerConfig(
store_root=tmp_path, store_root=tmp_path,
+14 -12
View File
@@ -9,8 +9,6 @@ from pydantic import ValidationError
from wf_mcp.broker import load_broker_config from wf_mcp.broker import load_broker_config
from wf_mcp.cli import build_parser, main from wf_mcp.cli import build_parser, main
from .test_support import local_temp_root
def _write_config(path: Path) -> None: def _write_config(path: Path) -> None:
path.write_text( path.write_text(
@@ -100,8 +98,10 @@ def test_build_parser_accepts_no_admin_tools_flag() -> None:
assert args.admin_tools is False assert args.admin_tools is False
def test_cli_connections_prints_configured_connections(capsys) -> None: def test_cli_connections_prints_configured_connections(
tmp_path = local_temp_root() / "cli_connections_test" capsys: pytest.CaptureFixture[str], tmp_path: Path
) -> None:
tmp_path = tmp_path / "cli_connections_test"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
_write_config(config_path) _write_config(config_path)
@@ -115,9 +115,9 @@ def test_cli_connections_prints_configured_connections(capsys) -> None:
def test_cli_catalog_prints_empty_catalog_when_not_refreshed( def test_cli_catalog_prints_empty_catalog_when_not_refreshed(
capsys, capsys: pytest.CaptureFixture[str], tmp_path: Path
) -> None: ) -> None:
tmp_path = local_temp_root() / "cli_catalog_test" tmp_path = tmp_path / "cli_catalog_test"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
_write_config(config_path) _write_config(config_path)
@@ -132,8 +132,10 @@ def test_cli_catalog_prints_empty_catalog_when_not_refreshed(
assert payload["prompts"] == [] assert payload["prompts"] == []
def test_cli_status_prints_connection_statuses(capsys) -> None: def test_cli_status_prints_connection_statuses(
tmp_path = local_temp_root() / "cli_status_test" capsys: pytest.CaptureFixture[str], tmp_path: Path
) -> None:
tmp_path = tmp_path / "cli_status_test"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
_write_config(config_path) _write_config(config_path)
@@ -159,8 +161,8 @@ def test_cli_status_prints_connection_statuses(capsys) -> None:
] ]
def test_load_broker_config_normalizes_typed_stdio_metadata() -> None: def test_load_broker_config_normalizes_typed_stdio_metadata(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "cli_typed_stdio_config_test" tmp_path = tmp_path / "cli_typed_stdio_config_test"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text( config_path.write_text(
@@ -195,8 +197,8 @@ def test_load_broker_config_normalizes_typed_stdio_metadata() -> None:
} }
def test_load_broker_config_rejects_bad_metadata_shape() -> None: def test_load_broker_config_rejects_bad_metadata_shape(tmp_path: Path) -> None:
tmp_path = local_temp_root() / "cli_bad_config_test" tmp_path = tmp_path / "cli_bad_config_test"
tmp_path.mkdir(parents=True, exist_ok=True) tmp_path.mkdir(parents=True, exist_ok=True)
config_path = tmp_path / "wf_mcp.config.json" config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text( config_path.write_text(
+8 -7
View File
@@ -2,13 +2,14 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Sequence from collections.abc import Sequence
from pathlib import Path
from wf_mcp.broker import WfMcpService from wf_mcp.broker import WfMcpService
from wf_mcp.events import EventBus, InMemoryEventSink, McpEvent, make_event from wf_mcp.events import EventBus, InMemoryEventSink, McpEvent, make_event
from wf_mcp.models import ConnectionConfig from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore from wf_mcp.storage import FileStore
from .test_support import FakeAdapter, echo_tool, local_temp_root from .test_support import FakeAdapter, echo_tool
def test_event_bus_fans_out_to_subscribers() -> None: def test_event_bus_fans_out_to_subscribers() -> None:
@@ -24,11 +25,11 @@ def test_event_bus_fans_out_to_subscribers() -> None:
assert seen_kinds == ["catalog_changed"] assert seen_kinds == ["catalog_changed"]
def test_service_records_events_through_event_bus() -> None: def test_service_records_events_through_event_bus(tmp_path: Path) -> None:
sink = InMemoryEventSink() sink = InMemoryEventSink()
bus = EventBus(sink) bus = EventBus(sink)
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "event_bus_service_store"), store=FileStore(tmp_path / "event_bus_service_store"),
event_bus=bus, event_bus=bus,
) )
@@ -40,8 +41,8 @@ def test_service_records_events_through_event_bus() -> None:
assert sink.list_events()[0] is service.list_events()[0] assert sink.list_events()[0] is service.list_events()[0]
def test_register_specs_emits_tool_and_catalog_change_events() -> None: def test_register_specs_emits_tool_and_catalog_change_events(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "spec_change_store")) service = WfMcpService(store=FileStore(tmp_path / "spec_change_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
@@ -66,8 +67,8 @@ def test_register_specs_emits_tool_and_catalog_change_events() -> None:
assert catalog_changed[0].payload["reason"] == "specs_registered" assert catalog_changed[0].payload["reason"] == "specs_registered"
def test_refresh_catalog_emits_capability_change_events() -> None: def test_refresh_catalog_emits_capability_change_events(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(local_temp_root() / "refresh_change_store")) service = WfMcpService(store=FileStore(tmp_path / "refresh_change_store"))
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
) )
+6 -3
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import sys import sys
from pathlib import Path
import mcp.types as mcp_types import mcp.types as mcp_types
import pytest import pytest
@@ -11,7 +12,7 @@ from mcp.client.stdio import StdioServerParameters, stdio_client
from wf_mcp.models import BrokerConfig, ConnectionConfig from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.proxy import create_proxy_client from wf_mcp.proxy import create_proxy_client
from .test_support import fixture_server_path, local_temp_root from .test_support import fixture_server_path
def test_fixture_server_initialize_capabilities_are_observable_directly() -> None: def test_fixture_server_initialize_capabilities_are_observable_directly() -> None:
@@ -40,9 +41,11 @@ def test_fixture_server_initialize_capabilities_are_observable_directly() -> Non
assert capabilities.logging is None assert capabilities.logging is None
def test_unified_proxy_initialize_capabilities_reflect_local_surface() -> None: def test_unified_proxy_initialize_capabilities_reflect_local_surface(
tmp_path: Path,
) -> None:
config = BrokerConfig( config = BrokerConfig(
store_root=local_temp_root() / "protocol_capabilities_store", store_root=tmp_path / "protocol_capabilities_store",
connections=[ connections=[
ConnectionConfig( ConnectionConfig(
id="fixture.personal", id="fixture.personal",
+12 -7
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
import asyncio import asyncio
import sys import sys
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from pathlib import Path
import mcp.types as mcp_types import mcp.types as mcp_types
import pytest import pytest
@@ -12,7 +13,7 @@ from mcp.client.stdio import StdioServerParameters, stdio_client
from wf_mcp.models import BrokerConfig, ConnectionConfig from wf_mcp.models import BrokerConfig, ConnectionConfig
from wf_mcp.proxy import create_proxy_client from wf_mcp.proxy import create_proxy_client
from .test_support import fixture_server_path, local_temp_root from .test_support import fixture_server_path
NotificationProbe = Callable[ NotificationProbe = Callable[
[Callable[[mcp_types.ServerNotification], None]], [Callable[[mcp_types.ServerNotification], None]],
@@ -70,9 +71,9 @@ def test_fixture_server_emits_observable_protocol_notifications_directly() -> No
assert "notifications/message" in methods assert "notifications/message" in methods
def _fixture_proxy_notification_methods() -> list[str]: def _fixture_proxy_notification_methods(tmp_path: Path) -> list[str]:
config = BrokerConfig( config = BrokerConfig(
store_root=local_temp_root() / "protocol_relay_store", store_root=tmp_path / "protocol_relay_store",
connections=[ connections=[
ConnectionConfig( ConnectionConfig(
id="fixture.personal", id="fixture.personal",
@@ -105,8 +106,10 @@ def _fixture_proxy_notification_methods() -> list[str]:
return _notification_methods(notifications) return _notification_methods(notifications)
def test_proxy_does_not_relay_generic_upstream_notifications_yet() -> None: def test_proxy_does_not_relay_generic_upstream_notifications_yet(
methods = _fixture_proxy_notification_methods() tmp_path: Path,
) -> None:
methods = _fixture_proxy_notification_methods(tmp_path)
# Stateful proxy sessions preserve FastMCP's supported request callbacks, # Stateful proxy sessions preserve FastMCP's supported request callbacks,
# but generic upstream change/update notification rebroadcast is separate # but generic upstream change/update notification rebroadcast is separate
@@ -124,7 +127,9 @@ def test_proxy_does_not_relay_generic_upstream_notifications_yet() -> None:
"data; valid string-valued MCP logging data is rejected upstream." "data; valid string-valued MCP logging data is rejected upstream."
), ),
) )
def test_proxy_relays_string_valued_upstream_log_when_fastmcp_supports_it() -> None: def test_proxy_relays_string_valued_upstream_log_when_fastmcp_supports_it(
methods = _fixture_proxy_notification_methods() tmp_path: Path,
) -> None:
methods = _fixture_proxy_notification_methods(tmp_path)
assert "notifications/message" in methods assert "notifications/message" in methods
+47 -36
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from pathlib import Path
from typing import Any from typing import Any
from wf_api.saved_subgraphs import resolve_saved_subgraph_tree from wf_api.saved_subgraphs import resolve_saved_subgraph_tree
@@ -17,11 +18,11 @@ from wf_mcp.models import ConnectionConfig
from wf_mcp.storage import FileStore from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from .test_support import echo_tool, input_binding, local_temp_root, output_binding from .test_support import echo_tool, input_binding, output_binding
def test_saved_subgraph_tree_loads_exact_child_artifact_version() -> None: def test_saved_subgraph_tree_loads_exact_child_artifact_version(tmp_path: Path) -> None:
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_tree") store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_tree")
parent = _parent_artifact() parent = _parent_artifact()
store.save_artifact(_leaf_artifact()) store.save_artifact(_leaf_artifact())
@@ -35,8 +36,8 @@ def test_saved_subgraph_tree_loads_exact_child_artifact_version() -> None:
assert resolution.artifacts_by_ref["workflow.child.v1"].version == 1 assert resolution.artifacts_by_ref["workflow.child.v1"].version == 1
def test_saved_subgraph_tree_reports_missing_child_artifact() -> None: def test_saved_subgraph_tree_reports_missing_child_artifact(tmp_path: Path) -> None:
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_missing") store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_missing")
resolution = resolve_saved_subgraph_tree( resolution = resolve_saved_subgraph_tree(
root_artifact=_parent_artifact(), root_artifact=_parent_artifact(),
@@ -48,8 +49,8 @@ def test_saved_subgraph_tree_reports_missing_child_artifact() -> None:
assert resolution.diagnostics[0].logical_ref == "workflow.child.v1" assert resolution.diagnostics[0].logical_ref == "workflow.child.v1"
def test_saved_subgraph_tree_reports_recursive_child_cycle() -> None: def test_saved_subgraph_tree_reports_recursive_child_cycle(tmp_path: Path) -> None:
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_cycle") store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_cycle")
parent = _parent_artifact() parent = _parent_artifact()
child = _parent_artifact( child = _parent_artifact(
artifact_id="child", artifact_id="child",
@@ -69,12 +70,14 @@ def test_saved_subgraph_tree_reports_recursive_child_cycle() -> None:
assert resolution.diagnostics[0].logical_ref == "workflow.parent.v1" assert resolution.diagnostics[0].logical_ref == "workflow.parent.v1"
def test_saved_child_uses_parent_deployment_binding_for_validation() -> None: def test_saved_child_uses_parent_deployment_binding_for_validation(
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_validate") tmp_path: Path,
) -> None:
store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_validate")
store.save_artifact(_parent_artifact()) store.save_artifact(_parent_artifact())
store.save_artifact(_leaf_artifact()) store.save_artifact(_leaf_artifact())
store.save_deployment(_deployment()) store.save_deployment(_deployment())
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal")) result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal"))
@@ -82,12 +85,12 @@ def test_saved_child_uses_parent_deployment_binding_for_validation() -> None:
assert result["diagnostics"] == [] assert result["diagnostics"] == []
def test_saved_child_missing_parent_binding_is_unrunnable() -> None: def test_saved_child_missing_parent_binding_is_unrunnable(tmp_path: Path) -> None:
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_unbound") store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_unbound")
store.save_artifact(_parent_artifact()) store.save_artifact(_parent_artifact())
store.save_artifact(_leaf_artifact()) store.save_artifact(_leaf_artifact())
store.save_deployment(_deployment(bindings={})) store.save_deployment(_deployment(bindings={}))
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal")) result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal"))
@@ -96,14 +99,14 @@ def test_saved_child_missing_parent_binding_is_unrunnable() -> None:
assert result["diagnostics"][0]["logical_ref"] == "demo.echo_tool" assert result["diagnostics"][0]["logical_ref"] == "demo.echo_tool"
def test_interrupting_saved_child_pauses_and_resumes_through_deployment_surface() -> ( def test_interrupting_saved_child_pauses_and_resumes_through_deployment_surface(
None tmp_path: Path,
): ) -> None:
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_interrupt") store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_interrupt")
store.save_artifact(_parent_artifact()) store.save_artifact(_parent_artifact())
store.save_artifact(_interrupting_child_artifact()) store.save_artifact(_interrupting_child_artifact())
store.save_deployment(_deployment()) store.save_deployment(_deployment())
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
validation = asyncio.run( validation = asyncio.run(
handlers.validate_deployment(deployment_id="parent.personal") handlers.validate_deployment(deployment_id="parent.personal")
@@ -125,7 +128,7 @@ def test_interrupting_saved_child_pauses_and_resumes_through_deployment_surface(
assert paused["interrupt"]["payload"]["question"] == "hello" assert paused["interrupt"]["payload"]["question"] == "hello"
# Durable resume must not rely on the process-local handler instance. # Durable resume must not rely on the process-local handler instance.
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
resumed = asyncio.run( resumed = asyncio.run(
handlers.resume_run( handlers.resume_run(
run_id=paused["run_id"], run_id=paused["run_id"],
@@ -138,12 +141,14 @@ def test_interrupting_saved_child_pauses_and_resumes_through_deployment_surface(
assert resumed["output"]["echoed"] == "world" assert resumed["output"]["echoed"] == "world"
def test_interrupted_saved_child_blocks_resume_until_pinned_source_returns() -> None: def test_interrupted_saved_child_blocks_resume_until_pinned_source_returns(
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_blocked") tmp_path: Path,
) -> None:
store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_blocked")
store.save_artifact(_parent_artifact()) store.save_artifact(_parent_artifact())
store.save_artifact(_interrupting_child_artifact(requires_demo=True)) store.save_artifact(_interrupting_child_artifact(requires_demo=True))
store.save_deployment(_deployment()) store.save_deployment(_deployment())
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
paused = asyncio.run( paused = asyncio.run(
handlers.run_deployment( handlers.run_deployment(
@@ -184,11 +189,13 @@ def test_interrupted_saved_child_blocks_resume_until_pinned_source_returns() ->
assert run_store.get_latest_checkpoint(paused["run_id"]).sequence == 2 assert run_store.get_latest_checkpoint(paused["run_id"]).sequence == 2
def test_missing_saved_child_is_unrunnable_on_deployment_surface() -> None: def test_missing_saved_child_is_unrunnable_on_deployment_surface(
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_missing_run") tmp_path: Path,
) -> None:
store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_missing_run")
store.save_artifact(_parent_artifact()) store.save_artifact(_parent_artifact())
store.save_deployment(_deployment()) store.save_deployment(_deployment())
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal")) result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal"))
@@ -197,8 +204,8 @@ def test_missing_saved_child_is_unrunnable_on_deployment_surface() -> None:
assert result["diagnostics"][0]["logical_ref"] == "workflow.child.v1" assert result["diagnostics"][0]["logical_ref"] == "workflow.child.v1"
def test_cyclic_saved_child_is_unrunnable_on_deployment_surface() -> None: def test_cyclic_saved_child_is_unrunnable_on_deployment_surface(tmp_path: Path) -> None:
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_cycle_run") store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_cycle_run")
store.save_artifact(_parent_artifact()) store.save_artifact(_parent_artifact())
store.save_artifact( store.save_artifact(
_parent_artifact( _parent_artifact(
@@ -208,7 +215,7 @@ def test_cyclic_saved_child_is_unrunnable_on_deployment_surface() -> None:
) )
) )
store.save_deployment(_deployment()) store.save_deployment(_deployment())
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal")) result = asyncio.run(handlers.validate_deployment(deployment_id="parent.personal"))
@@ -217,12 +224,14 @@ def test_cyclic_saved_child_is_unrunnable_on_deployment_surface() -> None:
assert result["diagnostics"][0]["logical_ref"] == "workflow.parent.v1" assert result["diagnostics"][0]["logical_ref"] == "workflow.parent.v1"
def test_saved_child_runs_natively_with_parent_deployment_binding() -> None: def test_saved_child_runs_natively_with_parent_deployment_binding(
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_run") tmp_path: Path,
) -> None:
store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_run")
store.save_artifact(_parent_artifact()) store.save_artifact(_parent_artifact())
store.save_artifact(_leaf_artifact()) store.save_artifact(_leaf_artifact())
store.save_deployment(_deployment()) store.save_deployment(_deployment())
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
result = asyncio.run( result = asyncio.run(
handlers.run_deployment( handlers.run_deployment(
@@ -236,8 +245,8 @@ def test_saved_child_runs_natively_with_parent_deployment_binding() -> None:
assert result["diagnostics"] == [] assert result["diagnostics"] == []
def test_nested_saved_child_inherits_root_deployment_binding() -> None: def test_nested_saved_child_inherits_root_deployment_binding(tmp_path: Path) -> None:
store = FileWorkflowArtifactStore(local_temp_root() / "saved_subgraph_nested_run") store = FileWorkflowArtifactStore(tmp_path / "saved_subgraph_nested_run")
store.save_artifact(_parent_artifact(child_artifact_id="middle")) store.save_artifact(_parent_artifact(child_artifact_id="middle"))
store.save_artifact( store.save_artifact(
_parent_artifact( _parent_artifact(
@@ -248,7 +257,7 @@ def test_nested_saved_child_inherits_root_deployment_binding() -> None:
) )
store.save_artifact(_leaf_artifact()) store.save_artifact(_leaf_artifact())
store.save_deployment(_deployment()) store.save_deployment(_deployment())
handlers = _handlers(store) handlers = _handlers(store, tmp_path)
result = asyncio.run( result = asyncio.run(
handlers.run_deployment( handlers.run_deployment(
@@ -381,8 +390,10 @@ def _deployment(*, bindings: dict[str, str] | None = None) -> WorkflowDeployment
) )
def _handlers(artifact_store: FileWorkflowArtifactStore) -> WorkflowSurfaceHandlers: def _handlers(
mcp_root = local_temp_root() / f"{artifact_store.root.name}_mcp" artifact_store: FileWorkflowArtifactStore, tmp_path: Path
) -> WorkflowSurfaceHandlers:
mcp_root = tmp_path / f"{artifact_store.root.name}_mcp"
service = WfMcpService( service = WfMcpService(
store=FileStore(mcp_root), store=FileStore(mcp_root),
artifact_store=artifact_store, artifact_store=artifact_store,
+28 -34
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio from pathlib import Path
import pytest import pytest
from mcp import McpError from mcp import McpError
@@ -16,7 +16,6 @@ from wf_sources_mcp.connections import mcp_source_connection_from_connection_con
from .test_support import ( from .test_support import (
everything_server_connection, everything_server_connection,
fixture_server_path, fixture_server_path,
local_temp_root,
sys, sys,
) )
@@ -70,8 +69,9 @@ class _WrappedToolsOnlyAdapter(_ToolsOnlyAdapter):
) )
def test_mcp_sdk_adapter_lists_and_calls_stdio_tool() -> None: @pytest.mark.asyncio
service = WfMcpService(store=FileStore(local_temp_root() / "sdk_adapter_store")) async def test_mcp_sdk_adapter_lists_and_calls_stdio_tool(tmp_path: Path) -> None:
service = WfMcpService(store=FileStore(tmp_path / "sdk_adapter_store"))
service.register_connection( service.register_connection(
ConnectionConfig( ConnectionConfig(
id="fixture.personal", id="fixture.personal",
@@ -87,7 +87,7 @@ def test_mcp_sdk_adapter_lists_and_calls_stdio_tool() -> None:
service.register_adapter("fixture", McpSdkAdapter()) service.register_adapter("fixture", McpSdkAdapter())
try: try:
asyncio.run(service.refresh_connection_catalog("fixture.personal")) await service.refresh_connection_catalog("fixture.personal")
except PermissionError as exc: except PermissionError as exc:
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}") pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
@@ -117,14 +117,10 @@ def test_mcp_sdk_adapter_lists_and_calls_stdio_tool() -> None:
} }
] ]
resource_result = asyncio.run( resource_result = await service.read_resource("fixture.personal.resource.welcome")
service.read_resource("fixture.personal.resource.welcome") prompt_result = await service.render_prompt(
) "fixture.personal.prompt.summarize",
prompt_result = asyncio.run( arguments={"text": "hello"},
service.render_prompt(
"fixture.personal.prompt.summarize",
arguments={"text": "hello"},
)
) )
assert ( assert (
resource_result["contents"][0]["text"] == "Welcome from the fixture MCP server." resource_result["contents"][0]["text"] == "Welcome from the fixture MCP server."
@@ -133,7 +129,7 @@ def test_mcp_sdk_adapter_lists_and_calls_stdio_tool() -> None:
prompt_result["messages"][0]["content"]["text"] prompt_result["messages"][0]["content"]["text"]
== "Summarize this text:\n\nhello" == "Summarize this text:\n\nhello"
) )
ping_result = asyncio.run(service.invoke_method("fixture.personal", "ping")) ping_result = await service.invoke_method("fixture.personal", "ping")
assert ping_result == {} assert ping_result == {}
adapter = McpSdkAdapter() adapter = McpSdkAdapter()
@@ -141,13 +137,11 @@ def test_mcp_sdk_adapter_lists_and_calls_stdio_tool() -> None:
source_connection = mcp_source_connection_from_connection_config( source_connection = mcp_source_connection_from_connection_config(
service.connections.get("fixture.personal") service.connections.get("fixture.personal")
) )
result = asyncio.run( result = await adapter.call_tool(
adapter.call_tool( connection=source_connection,
connection=source_connection, auth=None,
auth=None, tool_name="echo_tool",
tool_name="echo_tool", payload={"text": "hello"},
payload={"text": "hello"},
)
) )
except PermissionError as exc: except PermissionError as exc:
pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}") pytest.skip(f"stdio MCP transport is not permitted in this environment: {exc}")
@@ -155,21 +149,19 @@ def test_mcp_sdk_adapter_lists_and_calls_stdio_tool() -> None:
assert result.output == {"echoed": "hello"} assert result.output == {"echoed": "hello"}
def test_mcp_sdk_adapter_can_probe_everything_server() -> None: async def test_mcp_sdk_adapter_can_probe_everything_server(tmp_path: Path) -> None:
connection = everything_server_connection() connection = everything_server_connection()
if connection is None: if connection is None:
pytest.skip( pytest.skip(
"set MCP_EVERYTHING_COMMAND to enable the live everything-server integration test" "set MCP_EVERYTHING_COMMAND to enable the live everything-server integration test"
) )
service = WfMcpService( service = WfMcpService(store=FileStore(tmp_path / "everything_server_store"))
store=FileStore(local_temp_root() / "everything_server_store")
)
service.register_connection(connection) service.register_connection(connection)
service.register_adapter("everything", McpSdkAdapter()) service.register_adapter("everything", McpSdkAdapter())
try: try:
asyncio.run(service.refresh_connection_catalog("everything.default")) await service.refresh_connection_catalog("everything.default")
except PermissionError as exc: except PermissionError as exc:
pytest.skip(f"live MCP transport is not permitted in this environment: {exc}") pytest.skip(f"live MCP transport is not permitted in this environment: {exc}")
@@ -183,10 +175,10 @@ def test_mcp_sdk_adapter_can_probe_everything_server() -> None:
assert "prompts" in payload assert "prompts" in payload
def test_refresh_catalog_keeps_tools_when_optional_lists_are_unsupported() -> None: async def test_refresh_catalog_keeps_tools_when_optional_lists_are_unsupported(
service = WfMcpService( tmp_path: Path,
store=FileStore(local_temp_root() / "tools_only_server_store") ) -> None:
) service = WfMcpService(store=FileStore(tmp_path / "tools_only_server_store"))
service.register_connection( service.register_connection(
ConnectionConfig( ConnectionConfig(
id="tools_only.personal", id="tools_only.personal",
@@ -197,7 +189,7 @@ def test_refresh_catalog_keeps_tools_when_optional_lists_are_unsupported() -> No
) )
service.register_adapter("tools_only", _ToolsOnlyAdapter()) service.register_adapter("tools_only", _ToolsOnlyAdapter())
asyncio.run(service.refresh_connection_catalog("tools_only.personal")) await service.refresh_connection_catalog("tools_only.personal")
payload = service.get_catalog().as_payload() payload = service.get_catalog().as_payload()
assert payload["nodes"][0]["qualified_name"] == "tools_only.personal.echo_tool" assert payload["nodes"][0]["qualified_name"] == "tools_only.personal.echo_tool"
@@ -205,9 +197,11 @@ def test_refresh_catalog_keeps_tools_when_optional_lists_are_unsupported() -> No
assert payload["prompts"] == [] assert payload["prompts"] == []
def test_refresh_catalog_unwraps_taskgroup_method_not_found() -> None: async def test_refresh_catalog_unwraps_taskgroup_method_not_found(
tmp_path: Path,
) -> None:
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "wrapped_tools_only_server_store") store=FileStore(tmp_path / "wrapped_tools_only_server_store")
) )
service.register_connection( service.register_connection(
ConnectionConfig( ConnectionConfig(
@@ -219,7 +213,7 @@ def test_refresh_catalog_unwraps_taskgroup_method_not_found() -> None:
) )
service.register_adapter("wrapped_tools_only", _WrappedToolsOnlyAdapter()) service.register_adapter("wrapped_tools_only", _WrappedToolsOnlyAdapter())
asyncio.run(service.refresh_connection_catalog("wrapped_tools_only.personal")) await service.refresh_connection_catalog("wrapped_tools_only.personal")
payload = service.get_catalog().as_payload() payload = service.get_catalog().as_payload()
assert payload["nodes"][0]["qualified_name"] == ( assert payload["nodes"][0]["qualified_name"] == (
+2 -4
View File
@@ -7,11 +7,9 @@ from wf_mcp.connections import parse_connection_id
from wf_mcp.models import AuthRecord, CatalogSnapshot from wf_mcp.models import AuthRecord, CatalogSnapshot
from wf_mcp.storage import FileAuthStore, FileCatalogStore, FileStore from wf_mcp.storage import FileAuthStore, FileCatalogStore, FileStore
from .test_support import local_temp_root
def test_file_store_round_trips_auth(tmp_path) -> None:
def test_file_store_round_trips_auth() -> None: store = FileStore(tmp_path / "auth_store")
store = FileStore(local_temp_root() / "auth_store")
record = AuthRecord( record = AuthRecord(
connection_id="demo.personal", connection_id="demo.personal",
scheme="oauth", scheme="oauth",
+13 -3
View File
@@ -3,7 +3,8 @@ from __future__ import annotations
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, overload
from warnings import deprecated
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -47,8 +48,17 @@ def finalize_tool(
) )
def local_temp_root() -> Path: @deprecated("Use pytests tmp_path fixture instead")
root = Path("test-artifacts") / "wf_mcp_store" @overload
def local_temp_root() -> Path: ...
@overload
def local_temp_root(root_path: Path) -> Path: ...
def local_temp_root(root_path: Path | None = None) -> Path:
root = root_path or (Path("test-artifacts") / "wf_mcp_store")
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
return root return root
+66 -63
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from pathlib import Path
from wf_artifacts import ( from wf_artifacts import (
FileDraftWorkspaceStore, FileDraftWorkspaceStore,
@@ -15,7 +16,7 @@ from wf_mcp.storage import FileStore
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
from wf_mcp.workflow_surface.models import CreateMinimalDraftWorkspaceRequest from wf_mcp.workflow_surface.models import CreateMinimalDraftWorkspaceRequest
from ..test_support import echo_tool, local_temp_root from ..test_support import echo_tool
from .conftest import ( from .conftest import (
ContentOnlyOutputAdapter, ContentOnlyOutputAdapter,
echo_draft, echo_draft,
@@ -24,14 +25,12 @@ from .conftest import (
) )
def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known() -> ( def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known(
None tmp_path: Path,
): ) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_draft_bad_outcome")
local_temp_root() / "surface_draft_bad_outcome"
)
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_draft_bad_outcome_mcp"), store=FileStore(tmp_path / "surface_draft_bad_outcome_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
service.register_connection( service.register_connection(
@@ -48,14 +47,12 @@ def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known
assert payload["diagnostics"][0]["path"] == "routes.echo.typo" assert payload["diagnostics"][0]["path"] == "routes.echo.typo"
def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions() -> ( def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions(
None tmp_path: Path,
): ) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_draft_create")
local_temp_root() / "surface_draft_create"
)
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_draft_create_mcp"), store=FileStore(tmp_path / "surface_draft_create_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
service.register_connection( service.register_connection(
@@ -85,10 +82,10 @@ def test_workflow_surface_creates_artifact_from_draft_with_binding_suggestions()
assert artifact.required_capability_map()["demo.echo_tool"].logical_source == "demo" assert artifact.required_capability_map()["demo.echo_tool"].logical_source == "demo"
def test_workflow_surface_draft_artifact_requires_std_self_binding() -> None: def test_workflow_surface_draft_artifact_requires_std_self_binding(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "surface_draft_missing_std" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_draft_missing_std")
h = handlers(artifact_store) h = handlers(artifact_store)
asyncio.run( asyncio.run(
@@ -119,15 +116,15 @@ def test_workflow_surface_draft_artifact_requires_std_self_binding() -> None:
assert payload["diagnostics"][0]["logical_ref"] == "wf.std.replace" assert payload["diagnostics"][0]["logical_ref"] == "wf.std.replace"
def test_workflow_surface_validates_draft_workspace_with_live_outcomes() -> None: def test_workflow_surface_validates_draft_workspace_with_live_outcomes(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "surface_workspace_validate" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_workspace_validate")
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_validate_mcp"), store=FileStore(tmp_path / "surface_workspace_validate_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_workspace_validate_mcp" tmp_path / "surface_workspace_validate_mcp"
), ),
) )
service.register_connection( service.register_connection(
@@ -153,15 +150,15 @@ def test_workflow_surface_validates_draft_workspace_with_live_outcomes() -> None
assert fetched["status"] == "invalid" assert fetched["status"] == "invalid"
def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() -> None: def test_workflow_surface_creates_minimal_draft_workspace_with_error_route(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "surface_minimal_workspace" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_minimal_workspace")
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_minimal_workspace_mcp"), store=FileStore(tmp_path / "surface_minimal_workspace_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_minimal_workspace_mcp" tmp_path / "surface_minimal_workspace_mcp"
), ),
) )
service.register_connection( service.register_connection(
@@ -205,14 +202,16 @@ def test_workflow_surface_creates_minimal_draft_workspace_with_error_route() ->
] ]
def test_workflow_surface_minimal_draft_honors_explicit_error_message_source() -> None: def test_workflow_surface_minimal_draft_honors_explicit_error_message_source(
tmp_path: Path,
) -> None:
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_minimal_explicit_error_mcp"), store=FileStore(tmp_path / "surface_minimal_explicit_error_mcp"),
artifact_store=FileWorkflowArtifactStore( artifact_store=FileWorkflowArtifactStore(
local_temp_root() / "surface_minimal_explicit_error" tmp_path / "surface_minimal_explicit_error"
), ),
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_minimal_explicit_error_mcp" tmp_path / "surface_minimal_explicit_error_mcp"
), ),
) )
service.register_connection( service.register_connection(
@@ -266,14 +265,16 @@ def test_minimal_draft_request_accepts_structural_error_message_source() -> None
assert request.error_message_source.parts == ("error_message",) assert request.error_message_source.parts == ("error_message",)
def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() -> None: def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace(
tmp_path: Path,
) -> None:
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_minimal_canonical_mcp"), store=FileStore(tmp_path / "surface_minimal_canonical_mcp"),
artifact_store=FileWorkflowArtifactStore( artifact_store=FileWorkflowArtifactStore(
local_temp_root() / "surface_minimal_canonical" tmp_path / "surface_minimal_canonical"
), ),
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_minimal_canonical_mcp" tmp_path / "surface_minimal_canonical_mcp"
), ),
) )
h = WorkflowSurfaceHandlers(service) h = WorkflowSurfaceHandlers(service)
@@ -318,15 +319,17 @@ def test_workflow_surface_accepts_canonical_bindings_for_minimal_workspace() ->
] ]
def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> None: def test_workflow_surface_creates_draft_workspace_from_capability_hints(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_from_capability" tmp_path / "surface_workspace_from_capability"
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_from_capability_mcp"), store=FileStore(tmp_path / "surface_workspace_from_capability_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_workspace_from_capability_mcp" tmp_path / "surface_workspace_from_capability_mcp"
), ),
) )
service.register_connection( service.register_connection(
@@ -374,15 +377,13 @@ def test_workflow_surface_creates_draft_workspace_from_capability_hints() -> Non
] ]
def test_workflow_surface_creates_artifact_from_workspace() -> None: def test_workflow_surface_creates_artifact_from_workspace(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_workspace_artifact")
local_temp_root() / "surface_workspace_artifact"
)
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_artifact_mcp"), store=FileStore(tmp_path / "surface_workspace_artifact_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_workspace_artifact_mcp" tmp_path / "surface_workspace_artifact_mcp"
), ),
) )
service.register_connection( service.register_connection(
@@ -419,15 +420,17 @@ def test_workflow_surface_creates_artifact_from_workspace() -> None:
assert required.output_schema_snapshot is not None assert required.output_schema_snapshot is not None
def test_workflow_surface_workspace_artifact_infers_raw_concrete_dependency() -> None: def test_workflow_surface_workspace_artifact_infers_raw_concrete_dependency(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_artifact_raw_dependency" tmp_path / "surface_workspace_artifact_raw_dependency"
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_artifact_raw_mcp"), store=FileStore(tmp_path / "surface_workspace_artifact_raw_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_workspace_artifact_raw_mcp" tmp_path / "surface_workspace_artifact_raw_mcp"
), ),
) )
service.register_connection( service.register_connection(
@@ -459,15 +462,13 @@ def test_workflow_surface_workspace_artifact_infers_raw_concrete_dependency() ->
assert required.output_schema_snapshot is not None assert required.output_schema_snapshot is not None
def test_workflow_surface_creates_wrapper_from_workspace() -> None: def test_workflow_surface_creates_wrapper_from_workspace(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_workspace_wrapper")
local_temp_root() / "surface_workspace_wrapper"
)
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_wrapper_mcp"), store=FileStore(tmp_path / "surface_workspace_wrapper_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_workspace_wrapper_mcp" tmp_path / "surface_workspace_wrapper_mcp"
), ),
) )
service.register_connection( service.register_connection(
@@ -499,15 +500,17 @@ def test_workflow_surface_creates_wrapper_from_workspace() -> None:
assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool" assert artifact.plan["nodes"][0]["node"] == "demo.echo_tool"
def test_workflow_surface_low_confidence_draft_returns_patch_guidance() -> None: def test_workflow_surface_low_confidence_draft_returns_patch_guidance(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_workspace_low_confidence" tmp_path / "surface_workspace_low_confidence"
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_workspace_low_confidence_mcp"), store=FileStore(tmp_path / "surface_workspace_low_confidence_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
draft_workspace_store=FileDraftWorkspaceStore( draft_workspace_store=FileDraftWorkspaceStore(
local_temp_root() / "surface_workspace_low_confidence_mcp" tmp_path / "surface_workspace_low_confidence_mcp"
), ),
) )
service.register_connection( service.register_connection(
+47 -40
View File
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from pathlib import Path
from wf_artifacts import FileRunStore, FileWorkflowArtifactStore, WorkflowDeployment from wf_artifacts import FileRunStore, FileWorkflowArtifactStore, WorkflowDeployment
from wf_mcp.broker import WfMcpService from wf_mcp.broker import WfMcpService
@@ -15,7 +16,7 @@ from wf_platform import (
SourceVisibility, SourceVisibility,
) )
from ..test_support import echo_tool, local_temp_root from ..test_support import echo_tool
from .conftest import ( from .conftest import (
amount_tool, amount_tool,
changed_echo_tool, changed_echo_tool,
@@ -37,8 +38,8 @@ def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
assert plan.edges[0].outcome == "ok" assert plan.edges[0].outcome == "ok"
def test_workflow_surface_runs_non_interrupting_deployment() -> None: def test_workflow_surface_runs_non_interrupting_deployment(tmp_path: Path) -> None:
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_run") artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_run")
artifact_store.save_artifact(echo_artifact()) artifact_store.save_artifact(echo_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
WorkflowDeployment( WorkflowDeployment(
@@ -49,9 +50,9 @@ def test_workflow_surface_runs_non_interrupting_deployment() -> None:
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_mcp"), store=FileStore(tmp_path / "surface_run_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "surface_run_mcp"), run_store=FileRunStore(tmp_path / "surface_run_mcp"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -97,10 +98,10 @@ def test_workflow_surface_runs_non_interrupting_deployment() -> None:
assert traced["trace_truncated"] is False assert traced["trace_truncated"] is False
def test_workflow_surface_failed_deployment_exposes_error_on_run_and_inspect() -> None: def test_workflow_surface_failed_deployment_exposes_error_on_run_and_inspect(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "surface_failed_run_error" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_failed_run_error")
artifact_store.save_artifact(failing_artifact()) artifact_store.save_artifact(failing_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
WorkflowDeployment( WorkflowDeployment(
@@ -111,9 +112,9 @@ def test_workflow_surface_failed_deployment_exposes_error_on_run_and_inspect() -
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_failed_run_error_mcp"), store=FileStore(tmp_path / "surface_failed_run_error_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "surface_failed_run_error_mcp"), run_store=FileRunStore(tmp_path / "surface_failed_run_error_mcp"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -139,10 +140,10 @@ def test_workflow_surface_failed_deployment_exposes_error_on_run_and_inspect() -
assert inspected["next_actions"]["recommended_next_tool"] is None assert inspected["next_actions"]["recommended_next_tool"] is None
def test_workflow_surface_run_deployment_can_include_trace_detail() -> None: def test_workflow_surface_run_deployment_can_include_trace_detail(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "surface_run_trace_detail" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_run_trace_detail")
artifact_store.save_artifact(echo_artifact()) artifact_store.save_artifact(echo_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
WorkflowDeployment( WorkflowDeployment(
@@ -153,9 +154,9 @@ def test_workflow_surface_run_deployment_can_include_trace_detail() -> None:
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_trace_detail_mcp"), store=FileStore(tmp_path / "surface_run_trace_detail_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "surface_run_trace_detail_mcp"), run_store=FileRunStore(tmp_path / "surface_run_trace_detail_mcp"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -188,9 +189,11 @@ def test_workflow_surface_run_deployment_can_include_trace_detail() -> None:
assert validated["trace_truncated"] is False assert validated["trace_truncated"] is False
def test_workflow_surface_run_deployment_can_read_empty_trace_range() -> None: def test_workflow_surface_run_deployment_can_read_empty_trace_range(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore( artifact_store = FileWorkflowArtifactStore(
local_temp_root() / "surface_run_trace_empty_range" tmp_path / "surface_run_trace_empty_range"
) )
artifact_store.save_artifact(echo_artifact()) artifact_store.save_artifact(echo_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
@@ -202,9 +205,9 @@ def test_workflow_surface_run_deployment_can_read_empty_trace_range() -> None:
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_run_trace_empty_range_mcp"), store=FileStore(tmp_path / "surface_run_trace_empty_range_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "surface_run_trace_empty_range_mcp"), run_store=FileRunStore(tmp_path / "surface_run_trace_empty_range_mcp"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -227,8 +230,10 @@ def test_workflow_surface_run_deployment_can_read_empty_trace_range() -> None:
assert payload["trace_truncated"] is False assert payload["trace_truncated"] is False
def test_workflow_surface_runs_deployment_with_bound_node_spec_dependency() -> None: def test_workflow_surface_runs_deployment_with_bound_node_spec_dependency(
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_bound_node") tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_bound_node")
artifact_store.save_artifact(logical_echo_artifact()) artifact_store.save_artifact(logical_echo_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
WorkflowDeployment( WorkflowDeployment(
@@ -239,9 +244,9 @@ def test_workflow_surface_runs_deployment_with_bound_node_spec_dependency() -> N
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_bound_node_mcp"), store=FileStore(tmp_path / "surface_bound_node_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "surface_bound_node_mcp"), run_store=FileRunStore(tmp_path / "surface_bound_node_mcp"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -261,14 +266,14 @@ def test_workflow_surface_runs_deployment_with_bound_node_spec_dependency() -> N
assert payload["diagnostics"] == [] assert payload["diagnostics"] == []
def test_workflow_surface_runs_artifact_created_from_concrete_node_ref() -> None: def test_workflow_surface_runs_artifact_created_from_concrete_node_ref(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "surface_created_bound_node" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_created_bound_node")
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_created_bound_node_mcp"), store=FileStore(tmp_path / "surface_created_bound_node_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "surface_created_bound_node_mcp"), run_store=FileRunStore(tmp_path / "surface_created_bound_node_mcp"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
@@ -313,12 +318,12 @@ def test_workflow_surface_runs_artifact_created_from_concrete_node_ref() -> None
assert payload["diagnostics"] == [] assert payload["diagnostics"] == []
def test_workflow_surface_detects_drift_from_saved_node_spec_snapshot() -> None: def test_workflow_surface_detects_drift_from_saved_node_spec_snapshot(
artifact_store = FileWorkflowArtifactStore( tmp_path: Path,
local_temp_root() / "surface_created_drift" ) -> None:
) artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_created_drift")
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_created_drift_mcp"), store=FileStore(tmp_path / "surface_created_drift_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
) )
service.register_connection( service.register_connection(
@@ -379,8 +384,10 @@ def test_workflow_surface_detects_drift_from_saved_node_spec_snapshot() -> None:
assert payload["diagnostics"][0]["code"] == "schema_changed" assert payload["diagnostics"][0]["code"] == "schema_changed"
def test_workflow_surface_runs_deployment_with_bound_reducer_dependency() -> None: def test_workflow_surface_runs_deployment_with_bound_reducer_dependency(
artifact_store = FileWorkflowArtifactStore(local_temp_root() / "surface_reducer") tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "surface_reducer")
artifact_store.save_artifact(custom_reducer_artifact()) artifact_store.save_artifact(custom_reducer_artifact())
artifact_store.save_deployment( artifact_store.save_deployment(
WorkflowDeployment( WorkflowDeployment(
@@ -394,9 +401,9 @@ def test_workflow_surface_runs_deployment_with_bound_reducer_dependency() -> Non
) )
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(local_temp_root() / "surface_reducer_mcp"), store=FileStore(tmp_path / "surface_reducer_mcp"),
artifact_store=artifact_store, artifact_store=artifact_store,
run_store=FileRunStore(local_temp_root() / "surface_reducer_mcp"), run_store=FileRunStore(tmp_path / "surface_reducer_mcp"),
) )
service.register_connection( service.register_connection(
ConnectionConfig(id="demo.personal", server="demo", account="personal") ConnectionConfig(id="demo.personal", server="demo", account="personal")
Generated
+24
View File
@@ -298,6 +298,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
] ]
[[package]]
name = "execnet"
version = "2.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.136.3" version = "0.136.3"
@@ -661,6 +670,7 @@ dev = [
{ name = "basedpyright" }, { name = "basedpyright" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-xdist" },
{ name = "ruff" }, { name = "ruff" },
] ]
@@ -683,6 +693,7 @@ 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 = "pytest-asyncio", specifier = ">=1.4.0" },
{ name = "pytest-xdist", specifier = ">=3.8.0" },
{ name = "ruff", specifier = ">=0.15.15" }, { name = "ruff", specifier = ">=0.15.15" },
] ]
@@ -1079,6 +1090,19 @@ 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" }, { 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]]
name = "pytest-xdist"
version = "3.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "execnet" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.2" version = "1.2.2"