half an attempt to asyncify tests + deprecate old temp path creation
This commit is contained in:
@@ -213,3 +213,6 @@ wf*.config.json
|
|||||||
|
|
||||||
# Generated MCPB packages
|
# Generated MCPB packages
|
||||||
*.mcpb
|
*.mcpb
|
||||||
|
|
||||||
|
# pytest files
|
||||||
|
.pytest-tmp/
|
||||||
|
|||||||
+2
-1
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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())
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"] == (
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user