269 lines
8.7 KiB
Python
269 lines
8.7 KiB
Python
"""Tests for wf_api.deployments module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
|
|
from tests.wf_mcp.test_support import echo_tool
|
|
from wf_api.deployments import WorkflowDeploymentApi
|
|
from wf_artifacts import (
|
|
FileWorkflowArtifactStore,
|
|
RequiredCapability,
|
|
WorkflowArtifact,
|
|
WorkflowDeployment,
|
|
)
|
|
from wf_mcp.broker import WfMcpService
|
|
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
|
from wf_mcp.capabilities import DiscoveredTool
|
|
from wf_mcp.models import AuthRecord, ConnectionConfig
|
|
from wf_mcp.sdk import BackendAdapter
|
|
from wf_mcp.storage import FileStore
|
|
from wf_mcp.workflow_surface import WorkflowSurfaceHandlers
|
|
|
|
|
|
def _echo_artifact() -> WorkflowArtifact:
|
|
plan: dict[str, Any] = {
|
|
"name": "echo",
|
|
"input_schema": {
|
|
"type": "object",
|
|
"properties": {"text": {"type": "string"}},
|
|
"required": ["text"],
|
|
},
|
|
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
|
"output_schema": {
|
|
"type": "object",
|
|
"properties": {"echoed": {"type": "string"}},
|
|
"required": ["echoed"],
|
|
},
|
|
"start": "echo",
|
|
"nodes": [
|
|
{
|
|
"id": "echo",
|
|
"type": "node",
|
|
"node": "demo.personal.echo_tool",
|
|
"input": [
|
|
{
|
|
"path": {"root": "input", "parts": ["text"]},
|
|
"target": {"root": "local", "parts": ["text"]},
|
|
}
|
|
],
|
|
"output": [
|
|
{
|
|
"source": {"root": "local", "parts": ["echoed"]},
|
|
"target": {"root": "state", "parts": ["echoed"]},
|
|
}
|
|
],
|
|
}
|
|
],
|
|
"edges": [{"from": "echo", "outcome": "ok", "to": "__end__"}],
|
|
}
|
|
return WorkflowArtifact(
|
|
id="echo",
|
|
version=1,
|
|
title="Echo",
|
|
input_schema=plan["input_schema"],
|
|
output_schema=plan["output_schema"],
|
|
outcomes=("completed",),
|
|
plan=plan,
|
|
required_capabilities={
|
|
"demo.echo_tool": RequiredCapability(
|
|
ref="demo.echo_tool",
|
|
kind="node_spec",
|
|
)
|
|
},
|
|
)
|
|
|
|
|
|
def _deployment_api(
|
|
artifact_store: FileWorkflowArtifactStore,
|
|
*,
|
|
register_echo: bool = False,
|
|
) -> tuple[WorkflowDeploymentApi, WfMcpService]:
|
|
service = WfMcpService(
|
|
store=FileStore(
|
|
artifact_store.root / "deployments_mcp" / str(id(artifact_store))
|
|
),
|
|
artifact_store=artifact_store,
|
|
)
|
|
if register_echo:
|
|
service.register_connection(
|
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
)
|
|
service.register_specs("demo.personal", echo_tool)
|
|
context = context_from_service(service)
|
|
return WorkflowDeploymentApi(context), service
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_deployment_stores_and_returns_stable_fields(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_save")
|
|
api, _service = _deployment_api(artifact_store)
|
|
|
|
result = await api.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
).model_dump(mode="json")
|
|
)
|
|
|
|
assert result["saved"] is True
|
|
assert result["deployment_id"] == "echo.personal"
|
|
assert result["artifact_id"] == "echo"
|
|
assert result["artifact_version"] == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_deployments_returns_compact_summaries(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_list")
|
|
api, _service = _deployment_api(artifact_store)
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
)
|
|
)
|
|
|
|
result = await api.list_deployments()
|
|
|
|
assert len(result["deployments"]) == 1
|
|
assert result["deployments"][0]["id"] == "echo.personal"
|
|
assert result["deployments"][0]["binding_count"] == 1
|
|
assert "bindings" not in result["deployments"][0]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_deployments_returns_empty_without_artifact_store(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_no_store")
|
|
_api, service = _deployment_api(artifact_store)
|
|
context = replace(context_from_service(service), artifact_store=None)
|
|
api = WorkflowDeploymentApi(context)
|
|
|
|
result = await api.list_deployments()
|
|
|
|
assert result["deployments"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_deployment_removes_one(tmp_path: Path) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_delete")
|
|
api, _service = _deployment_api(artifact_store)
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
)
|
|
)
|
|
|
|
result = await api.delete_deployment(deployment_id="echo.personal")
|
|
|
|
assert result["deployment_id"] == "echo.personal"
|
|
assert result["deleted"] is True
|
|
assert artifact_store.list_deployments() == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_deployment_returns_runnable_for_valid_binding(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_validate_runnable")
|
|
api, service = _deployment_api(artifact_store, register_echo=True)
|
|
artifact_store.save_artifact(_echo_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
)
|
|
)
|
|
|
|
result = await api.validate_deployment(
|
|
deployment_id="echo.personal", live_check=False
|
|
)
|
|
|
|
assert result["status"] == "runnable"
|
|
assert result["diagnostics"] == []
|
|
|
|
|
|
class FailingLivenessAdapter:
|
|
async def list_tools(
|
|
self,
|
|
connection: ConnectionConfig,
|
|
auth: AuthRecord | None,
|
|
) -> list[DiscoveredTool]:
|
|
raise OSError("stdio process exited")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_validate_deployment_live_check_calls_live_checker(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_validate_live")
|
|
api, service = _deployment_api(artifact_store, register_echo=True)
|
|
artifact_store.save_artifact(_echo_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
)
|
|
)
|
|
service.register_adapter(
|
|
"demo",
|
|
cast(BackendAdapter, FailingLivenessAdapter()),
|
|
)
|
|
|
|
result = await api.validate_deployment(
|
|
deployment_id="echo.personal", live_check=True
|
|
)
|
|
|
|
assert result["status"] == "unrunnable"
|
|
assert result["diagnostics"][0]["code"] == "source_unreachable"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handler_delegation_for_validate_deployment(tmp_path: Path) -> None:
|
|
"""WorkflowSurfaceHandlers.validate_deployment delegates to WorkflowDeploymentApi."""
|
|
artifact_store = FileWorkflowArtifactStore(tmp_path / "deploy_delegation")
|
|
service = WfMcpService(
|
|
store=FileStore(artifact_store.root / "delegation_mcp"),
|
|
artifact_store=artifact_store,
|
|
)
|
|
artifact_store.save_artifact(_echo_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
)
|
|
)
|
|
|
|
h = WorkflowSurfaceHandlers(service)
|
|
context = context_from_service(service)
|
|
api = WorkflowDeploymentApi(context)
|
|
|
|
handler_result = await h.validate_deployment(deployment_id="echo.personal")
|
|
api_result = await api.validate_deployment(deployment_id="echo.personal")
|
|
|
|
assert handler_result["status"] == api_result["status"]
|
|
assert len(handler_result["diagnostics"]) == len(api_result["diagnostics"])
|
|
if handler_result["diagnostics"]:
|
|
assert (
|
|
handler_result["diagnostics"][0]["code"]
|
|
== api_result["diagnostics"][0]["code"]
|
|
)
|