dep validation of deployment
core wf valication uses a &mut List, this returns a List, hmmmm
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from wf_artifacts import (
|
||||
AvailableCapability,
|
||||
AvailableSource,
|
||||
DriftPolicy,
|
||||
RequiredCapability,
|
||||
WorkflowArtifact,
|
||||
WorkflowDeployment,
|
||||
validate_deployment_dependencies,
|
||||
)
|
||||
|
||||
|
||||
def required_capability(
|
||||
*,
|
||||
logical_source: str = "context7",
|
||||
capability_name: str = "query-docs",
|
||||
input_hash: str = "sha256:input",
|
||||
output_hash: str = "sha256:output",
|
||||
) -> RequiredCapability:
|
||||
return RequiredCapability(
|
||||
logical_source=logical_source,
|
||||
capability_name=capability_name,
|
||||
kind="tool",
|
||||
input_schema_hash=input_hash,
|
||||
input_schema_snapshot={"type": "object", "properties": {}},
|
||||
output_schema_hash=output_hash,
|
||||
output_schema_snapshot={"type": "object", "properties": {}},
|
||||
)
|
||||
|
||||
|
||||
def artifact_with(capability: RequiredCapability) -> WorkflowArtifact:
|
||||
return WorkflowArtifact(
|
||||
id="summarize_docs",
|
||||
version=1,
|
||||
title="Summarize Docs",
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
outcomes=("done",),
|
||||
plan={"name": "summarize_docs", "nodes": [], "edges": []},
|
||||
required_capabilities={
|
||||
f"{capability.logical_source}.{capability.capability_name}": capability
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def deployment(
|
||||
*,
|
||||
bindings: dict[str, str] | None = None,
|
||||
drift_policy: DriftPolicy = DriftPolicy.BLOCK,
|
||||
) -> WorkflowDeployment:
|
||||
return WorkflowDeployment(
|
||||
id="summarize_docs.personal",
|
||||
artifact_id="summarize_docs",
|
||||
artifact_version=1,
|
||||
bindings={"context7": "context7.personal"} if bindings is None else bindings,
|
||||
drift_policy=drift_policy,
|
||||
)
|
||||
|
||||
|
||||
def source(
|
||||
*,
|
||||
id: str = "context7.personal",
|
||||
enabled: bool = True,
|
||||
capability_name: str = "query-docs",
|
||||
input_hash: str = "sha256:input",
|
||||
output_hash: str = "sha256:output",
|
||||
) -> AvailableSource:
|
||||
return AvailableSource(
|
||||
id=id,
|
||||
enabled=enabled,
|
||||
capabilities={
|
||||
capability_name: AvailableCapability(
|
||||
name=capability_name,
|
||||
kind="tool",
|
||||
input_schema_hash=input_hash,
|
||||
output_schema_hash=output_hash,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_validate_deployment_accepts_matching_bound_capability() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(),
|
||||
sources=[source()],
|
||||
)
|
||||
|
||||
assert diagnostics == []
|
||||
|
||||
|
||||
def test_validate_deployment_reports_missing_binding() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(bindings={}),
|
||||
sources=[source()],
|
||||
)
|
||||
|
||||
assert len(diagnostics) == 1
|
||||
assert diagnostics[0].severity == "error"
|
||||
assert diagnostics[0].code == "binding_missing"
|
||||
assert diagnostics[0].logical_ref == "context7.query-docs"
|
||||
assert diagnostics[0].bound_source is None
|
||||
|
||||
|
||||
def test_validate_deployment_reports_missing_source() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(),
|
||||
sources=[],
|
||||
)
|
||||
|
||||
assert len(diagnostics) == 1
|
||||
assert diagnostics[0].severity == "error"
|
||||
assert diagnostics[0].code == "source_missing"
|
||||
assert diagnostics[0].logical_ref == "context7.query-docs"
|
||||
assert diagnostics[0].bound_source == "context7.personal"
|
||||
|
||||
|
||||
def test_validate_deployment_reports_disabled_source() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(),
|
||||
sources=[source(enabled=False)],
|
||||
)
|
||||
|
||||
assert len(diagnostics) == 1
|
||||
assert diagnostics[0].severity == "error"
|
||||
assert diagnostics[0].code == "source_disabled"
|
||||
assert diagnostics[0].bound_source == "context7.personal"
|
||||
|
||||
|
||||
def test_validate_deployment_reports_missing_capability() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(),
|
||||
sources=[source(capability_name="other-tool")],
|
||||
)
|
||||
|
||||
assert len(diagnostics) == 1
|
||||
assert diagnostics[0].severity == "error"
|
||||
assert diagnostics[0].code == "capability_missing"
|
||||
assert diagnostics[0].logical_ref == "context7.query-docs"
|
||||
|
||||
|
||||
def test_validate_deployment_blocks_changed_schema_by_default() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(),
|
||||
sources=[source(input_hash="sha256:changed")],
|
||||
)
|
||||
|
||||
assert len(diagnostics) == 1
|
||||
assert diagnostics[0].severity == "error"
|
||||
assert diagnostics[0].code == "schema_changed"
|
||||
|
||||
|
||||
def test_validate_deployment_warns_for_changed_schema_when_policy_warns() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(drift_policy=DriftPolicy.WARN),
|
||||
sources=[source(output_hash="sha256:changed")],
|
||||
)
|
||||
|
||||
assert len(diagnostics) == 1
|
||||
assert diagnostics[0].severity == "warning"
|
||||
assert diagnostics[0].code == "schema_changed"
|
||||
|
||||
|
||||
def test_validate_deployment_allows_changed_schema_when_policy_allows() -> None:
|
||||
diagnostics = validate_deployment_dependencies(
|
||||
artifact=artifact_with(required_capability()),
|
||||
deployment=deployment(drift_policy=DriftPolicy.ALLOW),
|
||||
sources=[source(input_hash="sha256:changed")],
|
||||
)
|
||||
|
||||
assert diagnostics == []
|
||||
@@ -79,9 +79,7 @@ def test_create_broker_server_exposes_tools_resources_and_prompts() -> None:
|
||||
server.call_tool("get_planner_catalog", {})
|
||||
)
|
||||
planner_catalog = cast(dict[str, Any], cast(object, planner_catalog_raw))
|
||||
planner_names = [
|
||||
node["qualified_name"] for node in planner_catalog["nodes"]
|
||||
]
|
||||
planner_names = [node["qualified_name"] for node in planner_catalog["nodes"]]
|
||||
assert "demo.personal.echo_tool" in planner_names
|
||||
assert "wf.mcp.call_tool" in planner_names
|
||||
assert "wf.std.runtime_error" in planner_names
|
||||
@@ -106,9 +104,10 @@ def test_broker_admin_tools_are_backed_by_wf_admin_source() -> None:
|
||||
|
||||
assert "list_spec_sources" in tool_names
|
||||
assert "get_planner_catalog" in tool_names
|
||||
assert "wf.admin.list_sources" in service.capability_sources[
|
||||
"wf.admin"
|
||||
].capabilities.tools
|
||||
assert (
|
||||
"wf.admin.list_sources"
|
||||
in service.capability_sources["wf.admin"].capabilities.tools
|
||||
)
|
||||
|
||||
|
||||
def test_build_service_from_config_registers_connections() -> None:
|
||||
|
||||
@@ -462,12 +462,8 @@ def test_service_excludes_disabled_connection_specs_from_planner_catalog() -> No
|
||||
service.capability_sources["demo.personal"].enabled = False
|
||||
|
||||
planner_payload = service.get_planner_catalog().as_payload()
|
||||
planner_names = [
|
||||
node["qualified_name"] for node in planner_payload["nodes"]
|
||||
]
|
||||
available_names = [
|
||||
entry.qualified_name for entry in service.list_available_specs()
|
||||
]
|
||||
planner_names = [node["qualified_name"] for node in planner_payload["nodes"]]
|
||||
available_names = [entry.qualified_name for entry in service.list_available_specs()]
|
||||
|
||||
assert "demo.personal.echo_tool" not in planner_names
|
||||
assert "demo.personal.echo_tool" not in available_names
|
||||
@@ -501,7 +497,9 @@ def test_service_preserves_disabled_connection_source_on_reregistration() -> Non
|
||||
assert "demo.personal.echo_tool" not in source.capabilities.node_specs
|
||||
|
||||
|
||||
def test_service_excludes_planner_hidden_connection_specs_from_planner_catalog() -> None:
|
||||
def test_service_excludes_planner_hidden_connection_specs_from_planner_catalog() -> (
|
||||
None
|
||||
):
|
||||
service = WfMcpService(
|
||||
store=FileStore(local_temp_root() / "hidden_connection_spec_store")
|
||||
)
|
||||
@@ -516,12 +514,8 @@ def test_service_excludes_planner_hidden_connection_specs_from_planner_catalog()
|
||||
)
|
||||
|
||||
planner_payload = service.get_planner_catalog().as_payload()
|
||||
planner_names = [
|
||||
node["qualified_name"] for node in planner_payload["nodes"]
|
||||
]
|
||||
available_names = [
|
||||
entry.qualified_name for entry in service.list_available_specs()
|
||||
]
|
||||
planner_names = [node["qualified_name"] for node in planner_payload["nodes"]]
|
||||
available_names = [entry.qualified_name for entry in service.list_available_specs()]
|
||||
|
||||
assert "demo.personal.echo_tool" not in planner_names
|
||||
assert "demo.personal.echo_tool" not in available_names
|
||||
|
||||
Reference in New Issue
Block a user