204 lines
6.9 KiB
Python
204 lines
6.9 KiB
Python
from __future__ import annotations
|
|
|
|
from wf_artifacts import (
|
|
DependencyDiagnostic,
|
|
DiagnosticSeverity,
|
|
DriftPolicy,
|
|
RequiredCapability,
|
|
WorkflowArtifact,
|
|
WorkflowDeployment,
|
|
)
|
|
|
|
|
|
def test_workflow_artifact_serializes_required_capability_contract() -> None:
|
|
capability = RequiredCapability(
|
|
ref="context7.query-docs",
|
|
kind="tool",
|
|
input_schema_hash="sha256:input",
|
|
input_schema_snapshot={"type": "object", "properties": {}},
|
|
output_schema_hash="sha256:output",
|
|
output_schema_snapshot={"type": "object", "properties": {}},
|
|
observed_concrete_source="context7.default",
|
|
observed_at_epoch_ms=123,
|
|
)
|
|
artifact = WorkflowArtifact(
|
|
id="summarize_docs",
|
|
version=1,
|
|
title="Summarize Docs",
|
|
description="Summarize retrieved documentation.",
|
|
input_schema={"type": "object", "properties": {}},
|
|
output_schema={"type": "object", "properties": {}},
|
|
outcomes=("done", "failed"),
|
|
plan={"name": "summarize_docs", "nodes": [], "edges": []},
|
|
required_capabilities=[capability],
|
|
created_from_catalog_version="catalog-1",
|
|
)
|
|
|
|
dumped = artifact.model_dump(mode="json")
|
|
|
|
assert dumped["id"] == "summarize_docs"
|
|
assert dumped["kind"] == "workflow"
|
|
assert dumped["version"] == 1
|
|
assert dumped["outcomes"] == ["done", "failed"]
|
|
required = dumped["required_capabilities"][0]
|
|
assert required["ref"] == {
|
|
"source": "context7",
|
|
"capability_key": "query-docs",
|
|
}
|
|
assert required["input_schema_hash"] == "sha256:input"
|
|
assert artifact.required_capability_map()["context7.query-docs"] == capability
|
|
|
|
|
|
def test_workflow_artifact_can_be_marked_as_wrapper_intent() -> None:
|
|
artifact = WorkflowArtifact(
|
|
id="normalize_status",
|
|
version=1,
|
|
title="Normalize Status",
|
|
kind="wrapper",
|
|
input_schema={"type": "object", "properties": {}},
|
|
output_schema={"type": "object", "properties": {}},
|
|
outcomes=("done", "needs_input"),
|
|
plan={"name": "normalize_status", "nodes": [], "edges": []},
|
|
)
|
|
|
|
dumped = artifact.model_dump(mode="json")
|
|
|
|
assert dumped["kind"] == "wrapper"
|
|
|
|
|
|
def test_workflow_artifact_accepts_legacy_required_capability_map_and_dumps_list() -> (
|
|
None
|
|
):
|
|
artifact = WorkflowArtifact.model_validate(
|
|
{
|
|
"id": "legacy_capabilities",
|
|
"version": 1,
|
|
"title": "Legacy Capabilities",
|
|
"input_schema": {"type": "object", "properties": {}},
|
|
"output_schema": {"type": "object", "properties": {}},
|
|
"outcomes": ["done"],
|
|
"plan": {"name": "legacy_capabilities", "nodes": [], "edges": []},
|
|
"required_capabilities": {
|
|
"demo.echo": {
|
|
"kind": "tool",
|
|
"input_schema_hash": "sha256:input",
|
|
"output_schema_hash": "sha256:output",
|
|
"observed_concrete_source": "demo.personal",
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
dumped = artifact.model_dump(mode="json")
|
|
required = dumped["required_capabilities"][0]
|
|
|
|
assert isinstance(dumped["required_capabilities"], list)
|
|
assert required["ref"] == {"source": "demo", "capability_key": "echo"}
|
|
assert required["observed_concrete_source"] == "demo.personal"
|
|
assert "logical_source" not in required
|
|
assert "capability_name" not in required
|
|
assert artifact.required_capability_map()["demo.echo"].logical_source == "demo"
|
|
|
|
|
|
def test_required_capability_accepts_legacy_logical_fields_but_dumps_ref_object() -> (
|
|
None
|
|
):
|
|
capability = RequiredCapability.model_validate(
|
|
{
|
|
"logical_source": "demo",
|
|
"capability_name": "foo.bar",
|
|
"kind": "node_spec",
|
|
}
|
|
)
|
|
|
|
assert capability.logical_source == "demo"
|
|
assert capability.capability_name == "foo.bar"
|
|
assert capability.model_dump(mode="json")["ref"] == {
|
|
"source": "demo",
|
|
"capability_key": "foo.bar",
|
|
}
|
|
|
|
|
|
def test_workflow_artifact_legacy_required_capability_map_is_best_effort() -> None:
|
|
artifact = WorkflowArtifact.model_validate(
|
|
{
|
|
"id": "legacy_dotted_capability",
|
|
"version": 1,
|
|
"title": "Legacy Dotted Capability",
|
|
"input_schema": {"type": "object", "properties": {}},
|
|
"output_schema": {"type": "object", "properties": {}},
|
|
"outcomes": ["done"],
|
|
"plan": {"name": "legacy_dotted_capability", "nodes": [], "edges": []},
|
|
"required_capabilities": {
|
|
"demo.foo.bar": {"kind": "node_spec"},
|
|
},
|
|
}
|
|
)
|
|
|
|
required = artifact.model_dump(mode="json")["required_capabilities"][0]
|
|
|
|
assert required["ref"] == {
|
|
"source": "demo.foo",
|
|
"capability_key": "bar",
|
|
}
|
|
|
|
|
|
def test_workflow_deployment_binds_logical_sources_to_concrete_sources() -> None:
|
|
deployment = WorkflowDeployment(
|
|
id="summarize_docs.personal",
|
|
artifact_id="summarize_docs",
|
|
artifact_version=1,
|
|
bindings=[
|
|
{"logical_source": "context7", "concrete_source": "context7.personal"}
|
|
],
|
|
drift_policy=DriftPolicy.BLOCK,
|
|
)
|
|
|
|
dumped = deployment.model_dump(mode="json")
|
|
|
|
assert dumped["id"] == "summarize_docs.personal"
|
|
assert dumped["artifact_id"] == "summarize_docs"
|
|
assert dumped["artifact_version"] == 1
|
|
assert dumped["bindings"][0]["logical_source"] == "context7"
|
|
assert dumped["bindings"][0]["concrete_source"] == "context7.personal"
|
|
assert dumped["drift_policy"] == "block"
|
|
assert deployment.binding_map()["context7"] == "context7.personal"
|
|
|
|
|
|
def test_workflow_deployment_accepts_legacy_binding_map_and_dumps_list() -> None:
|
|
deployment = WorkflowDeployment.model_validate(
|
|
{
|
|
"id": "legacy_bindings.personal",
|
|
"artifact_id": "legacy_bindings",
|
|
"artifact_version": 1,
|
|
"bindings": {"demo": "demo.personal"},
|
|
}
|
|
)
|
|
|
|
dumped = deployment.model_dump(mode="json")
|
|
binding = dumped["bindings"][0]
|
|
|
|
assert isinstance(dumped["bindings"], list)
|
|
assert binding["logical_source"] == "demo"
|
|
assert binding["concrete_source"] == "demo.personal"
|
|
assert deployment.binding_map()["demo"] == "demo.personal"
|
|
|
|
|
|
def test_dependency_diagnostic_is_structured() -> None:
|
|
diagnostic = DependencyDiagnostic(
|
|
severity=DiagnosticSeverity.ERROR,
|
|
code="capability_missing",
|
|
logical_ref="context7.query-docs",
|
|
bound_source="context7.default",
|
|
message="Bound source no longer exposes query-docs.",
|
|
repair_hint="Refresh catalog or bind context7 to another compatible source.",
|
|
)
|
|
|
|
dumped = diagnostic.model_dump(mode="json")
|
|
|
|
assert dumped["severity"] == "error"
|
|
assert dumped["code"] == "capability_missing"
|
|
assert dumped["logical_ref"] == "context7.query-docs"
|
|
assert dumped["bound_source"] == "context7.default"
|
|
assert dumped["repair_hint"].startswith("Refresh catalog")
|