fmt
This commit is contained in:
@@ -6,61 +6,63 @@ from wf_core.run_state import RunState
|
|||||||
|
|
||||||
def build_raw_canonical_workflow() -> Workflow:
|
def build_raw_canonical_workflow() -> Workflow:
|
||||||
"""Build a raw core workflow using the canonical post-migration shape."""
|
"""Build a raw core workflow using the canonical post-migration shape."""
|
||||||
return Workflow.model_validate({
|
return Workflow.model_validate(
|
||||||
"name": "raw_canonical_echo",
|
{
|
||||||
"input_schema": {
|
"name": "raw_canonical_echo",
|
||||||
"type": "object",
|
"input_schema": {
|
||||||
"properties": {"text": {"type": "string"}},
|
"type": "object",
|
||||||
"required": ["text"],
|
"properties": {"text": {"type": "string"}},
|
||||||
},
|
"required": ["text"],
|
||||||
"state_schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"reducer": "wf.std.replace",
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
"state_schema": {
|
||||||
"output_schema": {
|
"type": "object",
|
||||||
"type": "object",
|
"properties": {
|
||||||
"properties": {"message": {"type": "string"}},
|
"message": {
|
||||||
"required": ["message"],
|
"type": "string",
|
||||||
},
|
"reducer": "wf.std.replace",
|
||||||
"node_defs": [
|
}
|
||||||
{
|
},
|
||||||
"name": "format_text",
|
},
|
||||||
"input_schema": {
|
"output_schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {"message": {"type": "string"}},
|
||||||
"text": {"type": "string"},
|
"required": ["message"],
|
||||||
"prefix": {"type": "string"},
|
},
|
||||||
|
"node_defs": [
|
||||||
|
{
|
||||||
|
"name": "format_text",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"text": {"type": "string"},
|
||||||
|
"prefix": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["text", "prefix"],
|
||||||
},
|
},
|
||||||
"required": ["text", "prefix"],
|
"output_schema": {
|
||||||
},
|
"type": "object",
|
||||||
"output_schema": {
|
"properties": {"message": {"type": "string"}},
|
||||||
"type": "object",
|
"required": ["message"],
|
||||||
"properties": {"message": {"type": "string"}},
|
},
|
||||||
"required": ["message"],
|
"outcomes": ["ok"],
|
||||||
},
|
}
|
||||||
"outcomes": ["ok"],
|
],
|
||||||
}
|
"start": "format",
|
||||||
],
|
"nodes": [
|
||||||
"start": "format",
|
{
|
||||||
"nodes": [
|
"id": "format",
|
||||||
{
|
"type": "node",
|
||||||
"id": "format",
|
"node": "format_text",
|
||||||
"type": "node",
|
"input": [
|
||||||
"node": "format_text",
|
{"target": "text", "path": "input.text"},
|
||||||
"input": [
|
{"target": "prefix", "value": "raw:"},
|
||||||
{"target": "text", "path": "input.text"},
|
],
|
||||||
{"target": "prefix", "value": "raw:"},
|
"output": [{"source": "message", "target": "state.message"}],
|
||||||
],
|
}
|
||||||
"output": [{"source": "message", "target": "state.message"}],
|
],
|
||||||
}
|
"edges": [{"from": "format", "outcome": "ok", "to": END}],
|
||||||
],
|
}
|
||||||
"edges": [{"from": "format", "outcome": "ok", "to": END}],
|
)
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def build_raw_canonical_registry():
|
def build_raw_canonical_registry():
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ def artifact_catalog_entry(
|
|||||||
diagnostics: list[DependencyDiagnostic] | tuple[DependencyDiagnostic, ...] = (),
|
diagnostics: list[DependencyDiagnostic] | tuple[DependencyDiagnostic, ...] = (),
|
||||||
) -> WorkflowArtifactCatalogEntry:
|
) -> WorkflowArtifactCatalogEntry:
|
||||||
"""Project an artifact as a catalog entry without exposing its internal plan."""
|
"""Project an artifact as a catalog entry without exposing its internal plan."""
|
||||||
required_sources = sorted({
|
required_sources = sorted(
|
||||||
capability.logical_source
|
{
|
||||||
for capability in artifact.required_capability_map().values()
|
capability.logical_source
|
||||||
})
|
for capability in artifact.required_capability_map().values()
|
||||||
|
}
|
||||||
|
)
|
||||||
return WorkflowArtifactCatalogEntry(
|
return WorkflowArtifactCatalogEntry(
|
||||||
name=artifact_node_name(artifact),
|
name=artifact_node_name(artifact),
|
||||||
artifact_id=artifact.id,
|
artifact_id=artifact.id,
|
||||||
|
|||||||
@@ -319,22 +319,26 @@ class WfMcpService:
|
|||||||
statuses: list[dict[str, Any]] = []
|
statuses: list[dict[str, Any]] = []
|
||||||
for connection in self.connections.list_all():
|
for connection in self.connections.list_all():
|
||||||
snapshot = self.store.load_catalog(connection.id)
|
snapshot = self.store.load_catalog(connection.id)
|
||||||
statuses.append({
|
statuses.append(
|
||||||
"connection_id": connection.id,
|
{
|
||||||
"server": connection.server,
|
"connection_id": connection.id,
|
||||||
"account": connection.account,
|
"server": connection.server,
|
||||||
"enabled": connection.enabled,
|
"account": connection.account,
|
||||||
"has_snapshot": snapshot is not None,
|
"enabled": connection.enabled,
|
||||||
"fetched_at_epoch_ms": None
|
"has_snapshot": snapshot is not None,
|
||||||
if snapshot is None
|
"fetched_at_epoch_ms": None
|
||||||
else snapshot.fetched_at_epoch_ms,
|
if snapshot is None
|
||||||
"max_age_seconds": None
|
else snapshot.fetched_at_epoch_ms,
|
||||||
if snapshot is None
|
"max_age_seconds": None
|
||||||
else snapshot.max_age_seconds,
|
if snapshot is None
|
||||||
"node_count": 0 if snapshot is None else len(snapshot.nodes),
|
else snapshot.max_age_seconds,
|
||||||
"resource_count": 0 if snapshot is None else len(snapshot.resources),
|
"node_count": 0 if snapshot is None else len(snapshot.nodes),
|
||||||
"prompt_count": 0 if snapshot is None else len(snapshot.prompts),
|
"resource_count": 0
|
||||||
})
|
if snapshot is None
|
||||||
|
else len(snapshot.resources),
|
||||||
|
"prompt_count": 0 if snapshot is None else len(snapshot.prompts),
|
||||||
|
}
|
||||||
|
)
|
||||||
return statuses
|
return statuses
|
||||||
|
|
||||||
def list_resources(
|
def list_resources(
|
||||||
|
|||||||
+37
-27
@@ -94,20 +94,26 @@ async def _refresh_all(service, connection_id: str | None) -> list[dict[str, Any
|
|||||||
try:
|
try:
|
||||||
await service.refresh_connection_catalog(target_id)
|
await service.refresh_connection_catalog(target_id)
|
||||||
snapshot = service.get_connection_snapshot(target_id)
|
snapshot = service.get_connection_snapshot(target_id)
|
||||||
results.append({
|
results.append(
|
||||||
"connection_id": target_id,
|
{
|
||||||
"refreshed": snapshot is not None,
|
"connection_id": target_id,
|
||||||
"node_count": 0 if snapshot is None else len(snapshot.nodes),
|
"refreshed": snapshot is not None,
|
||||||
"resource_count": 0 if snapshot is None else len(snapshot.resources),
|
"node_count": 0 if snapshot is None else len(snapshot.nodes),
|
||||||
"prompt_count": 0 if snapshot is None else len(snapshot.prompts),
|
"resource_count": 0
|
||||||
})
|
if snapshot is None
|
||||||
|
else len(snapshot.resources),
|
||||||
|
"prompt_count": 0 if snapshot is None else len(snapshot.prompts),
|
||||||
|
}
|
||||||
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
results.append({
|
results.append(
|
||||||
"connection_id": target_id,
|
{
|
||||||
"refreshed": False,
|
"connection_id": target_id,
|
||||||
"error_type": type(exc).__name__,
|
"refreshed": False,
|
||||||
"error": str(exc),
|
"error_type": type(exc).__name__,
|
||||||
})
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@@ -132,16 +138,18 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
service = _service_from_config(args.config)
|
service = _service_from_config(args.config)
|
||||||
|
|
||||||
if args.command == "connections":
|
if args.command == "connections":
|
||||||
_json_dump([
|
_json_dump(
|
||||||
{
|
[
|
||||||
"id": connection.id,
|
{
|
||||||
"server": connection.server,
|
"id": connection.id,
|
||||||
"account": connection.account,
|
"server": connection.server,
|
||||||
"enabled": connection.enabled,
|
"account": connection.account,
|
||||||
"metadata": connection.metadata,
|
"enabled": connection.enabled,
|
||||||
}
|
"metadata": connection.metadata,
|
||||||
for connection in service.connections.list_all()
|
}
|
||||||
])
|
for connection in service.connections.list_all()
|
||||||
|
]
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
if args.command == "status":
|
if args.command == "status":
|
||||||
@@ -154,10 +162,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
|
|
||||||
if args.command == "refresh":
|
if args.command == "refresh":
|
||||||
results = asyncio.run(_refresh_all(service, args.connection_id))
|
results = asyncio.run(_refresh_all(service, args.connection_id))
|
||||||
_json_dump({
|
_json_dump(
|
||||||
"results": results,
|
{
|
||||||
"catalog": service.get_catalog().as_payload(),
|
"results": results,
|
||||||
})
|
"catalog": service.get_catalog().as_payload(),
|
||||||
|
}
|
||||||
|
)
|
||||||
if any(not result["refreshed"] for result in results):
|
if any(not result["refreshed"] for result in results):
|
||||||
return 1
|
return 1
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ def connection_to_fastmcp_server_config(
|
|||||||
def broker_config_to_fastmcp_config(config: BrokerConfig) -> MCPConfig:
|
def broker_config_to_fastmcp_config(config: BrokerConfig) -> MCPConfig:
|
||||||
"""Convert broker config into FastMCP's multi-server config object."""
|
"""Convert broker config into FastMCP's multi-server config object."""
|
||||||
validate_proxy_config(config)
|
validate_proxy_config(config)
|
||||||
return MCPConfig.from_dict({
|
return MCPConfig.from_dict(
|
||||||
"mcpServers": {
|
{
|
||||||
connection.id: connection_to_fastmcp_server_config(connection)
|
"mcpServers": {
|
||||||
for connection in config.connections
|
connection.id: connection_to_fastmcp_server_config(connection)
|
||||||
if connection.enabled
|
for connection in config.connections
|
||||||
|
if connection.enabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|||||||
@@ -46,11 +46,13 @@ class ResourceLinkRewritingTool(Tool):
|
|||||||
rewrite_uri: Callable[[str], str],
|
rewrite_uri: Callable[[str], str],
|
||||||
) -> ResourceLinkRewritingTool:
|
) -> ResourceLinkRewritingTool:
|
||||||
"""Copy one tool's public schema while replacing only execution."""
|
"""Copy one tool's public schema while replacing only execution."""
|
||||||
return cls.model_validate({
|
return cls.model_validate(
|
||||||
**tool.model_dump(),
|
{
|
||||||
"parent_tool": tool,
|
**tool.model_dump(),
|
||||||
"rewrite_uri": rewrite_uri,
|
"parent_tool": tool,
|
||||||
})
|
"rewrite_uri": rewrite_uri,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ResourceLinkNamespace(Transform):
|
class ResourceLinkNamespace(Transform):
|
||||||
|
|||||||
@@ -174,9 +174,7 @@ class CapabilitySource:
|
|||||||
reducers=_preview_names(
|
reducers=_preview_names(
|
||||||
self.capabilities.reducers, SOURCE_PREVIEW_LIMIT
|
self.capabilities.reducers, SOURCE_PREVIEW_LIMIT
|
||||||
),
|
),
|
||||||
prompts=_preview_names(
|
prompts=_preview_names(self.capabilities.prompts, SOURCE_PREVIEW_LIMIT),
|
||||||
self.capabilities.prompts, SOURCE_PREVIEW_LIMIT
|
|
||||||
),
|
|
||||||
resources=_preview_names(
|
resources=_preview_names(
|
||||||
self.capabilities.resources, SOURCE_PREVIEW_LIMIT
|
self.capabilities.resources, SOURCE_PREVIEW_LIMIT
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -126,16 +126,18 @@ def test_output_bindings_validate_exact_state_schema_before_mutation() -> None:
|
|||||||
|
|
||||||
def test_output_bindings_validate_declared_parent_schema_before_mutation() -> None:
|
def test_output_bindings_validate_declared_parent_schema_before_mutation() -> None:
|
||||||
workflow = _workflow_from_state_schema(
|
workflow = _workflow_from_state_schema(
|
||||||
StateSchema.model_validate({
|
StateSchema.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {
|
"type": "object",
|
||||||
"person": {
|
"properties": {
|
||||||
"type": "object",
|
"person": {
|
||||||
"properties": {"name": {"type": "string"}},
|
"type": "object",
|
||||||
"additionalProperties": False,
|
"properties": {"name": {"type": "string"}},
|
||||||
}
|
"additionalProperties": False,
|
||||||
},
|
}
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
state = {"person": {"name": "old"}}
|
state = {"person": {"name": "old"}}
|
||||||
|
|
||||||
@@ -204,9 +206,9 @@ def _workflow_with_node() -> Workflow:
|
|||||||
return Workflow(
|
return Workflow(
|
||||||
name="canonical_output",
|
name="canonical_output",
|
||||||
input_schema=SchemaRef(type="object", properties={}),
|
input_schema=SchemaRef(type="object", properties={}),
|
||||||
state_schema=StateSchema.from_field_map({
|
state_schema=StateSchema.from_field_map(
|
||||||
"person.name": StateField(type="string")
|
{"person.name": StateField(type="string")}
|
||||||
}),
|
),
|
||||||
output_schema=SchemaRef(
|
output_schema=SchemaRef(
|
||||||
type="object", properties={"person": {"type": "object"}}
|
type="object", properties={"person": {"type": "object"}}
|
||||||
),
|
),
|
||||||
@@ -223,12 +225,16 @@ def _workflow_with_node() -> Workflow:
|
|||||||
],
|
],
|
||||||
start="rename",
|
start="rename",
|
||||||
nodes=[
|
nodes=[
|
||||||
NodeUse.model_validate({
|
NodeUse.model_validate(
|
||||||
"id": "rename",
|
{
|
||||||
"type": "node",
|
"id": "rename",
|
||||||
"node": "rename",
|
"type": "node",
|
||||||
"output": [{"source": "person.name", "target": "state.person.name"}],
|
"node": "rename",
|
||||||
})
|
"output": [
|
||||||
|
{"source": "person.name", "target": "state.person.name"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
],
|
],
|
||||||
edges=[Edge.model_validate({"from": "rename", "outcome": "ok", "to": END})],
|
edges=[Edge.model_validate({"from": "rename", "outcome": "ok", "to": END})],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -118,18 +118,15 @@ def test_canonical_binding_json_schema_describes_nested_fields():
|
|||||||
input_value = defs["InputValueBinding"]
|
input_value = defs["InputValueBinding"]
|
||||||
output = defs["OutputBinding"]
|
output = defs["OutputBinding"]
|
||||||
|
|
||||||
assert "whole node input payload" in input_path["properties"]["target"][
|
assert (
|
||||||
"description"
|
"whole node input payload" in input_path["properties"]["target"]["description"]
|
||||||
]
|
)
|
||||||
assert "input, state, or context" in input_path["properties"]["path"][
|
assert "input, state, or context" in input_path["properties"]["path"]["description"]
|
||||||
"description"
|
assert (
|
||||||
]
|
"Literal JSON-compatible value"
|
||||||
assert "Literal JSON-compatible value" in input_value["properties"]["value"][
|
in input_value["properties"]["value"]["description"]
|
||||||
"description"
|
)
|
||||||
]
|
assert "whole node output payload" in output["properties"]["source"]["description"]
|
||||||
assert "whole node output payload" in output["properties"]["source"][
|
|
||||||
"description"
|
|
||||||
]
|
|
||||||
assert "Bare state is invalid" in output["properties"]["target"]["description"]
|
assert "Bare state is invalid" in output["properties"]["target"]["description"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -178,10 +178,12 @@ def _workflow(
|
|||||||
|
|
||||||
return Workflow(
|
return Workflow(
|
||||||
name="mapping_validation",
|
name="mapping_validation",
|
||||||
input_schema=SchemaRef.model_validate({
|
input_schema=SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {"person": {"type": "object"}},
|
"type": "object",
|
||||||
}),
|
"properties": {"person": {"type": "object"}},
|
||||||
|
}
|
||||||
|
),
|
||||||
state_schema=StateSchema.from_field_map(
|
state_schema=StateSchema.from_field_map(
|
||||||
state_fields or {"person": StateField(type="object")}
|
state_fields or {"person": StateField(type="object")}
|
||||||
),
|
),
|
||||||
@@ -189,14 +191,18 @@ def _workflow(
|
|||||||
node_defs=[
|
node_defs=[
|
||||||
NodeDef(
|
NodeDef(
|
||||||
name="tool",
|
name="tool",
|
||||||
input_schema=SchemaRef.model_validate({
|
input_schema=SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {"user": {"type": "object"}},
|
"type": "object",
|
||||||
}),
|
"properties": {"user": {"type": "object"}},
|
||||||
output_schema=SchemaRef.model_validate({
|
}
|
||||||
"type": "object",
|
),
|
||||||
"properties": {"user": {"type": "object"}},
|
output_schema=SchemaRef.model_validate(
|
||||||
}),
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"user": {"type": "object"}},
|
||||||
|
}
|
||||||
|
),
|
||||||
outcomes=["ok"],
|
outcomes=["ok"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
+131
-109
@@ -19,52 +19,54 @@ from wf_core import (
|
|||||||
|
|
||||||
|
|
||||||
def test_canonical_bindings_resolve_input_values_paths_and_explicit_null() -> None:
|
def test_canonical_bindings_resolve_input_values_paths_and_explicit_null() -> None:
|
||||||
workflow = Workflow.model_validate({
|
workflow = Workflow.model_validate(
|
||||||
"name": "canonical",
|
{
|
||||||
"input_schema": {
|
"name": "canonical",
|
||||||
"type": "object",
|
"input_schema": {
|
||||||
"properties": {"message": {"type": "string"}},
|
"type": "object",
|
||||||
},
|
"properties": {"message": {"type": "string"}},
|
||||||
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
},
|
||||||
"output_schema": {
|
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
||||||
"type": "object",
|
"output_schema": {
|
||||||
"properties": {"echoed": {"type": "string"}},
|
"type": "object",
|
||||||
},
|
"properties": {"echoed": {"type": "string"}},
|
||||||
"start": "echo",
|
},
|
||||||
"node_defs": [
|
"start": "echo",
|
||||||
{
|
"node_defs": [
|
||||||
"name": "echo",
|
{
|
||||||
"input_schema": {
|
"name": "echo",
|
||||||
"type": "object",
|
"input_schema": {
|
||||||
"properties": {
|
"type": "object",
|
||||||
"message": {"type": "string"},
|
"properties": {
|
||||||
"mode": {"type": "string"},
|
"message": {"type": "string"},
|
||||||
"maybe": {"type": "null"},
|
"mode": {"type": "string"},
|
||||||
|
"maybe": {"type": "null"},
|
||||||
|
},
|
||||||
|
"required": ["message", "mode", "maybe"],
|
||||||
},
|
},
|
||||||
"required": ["message", "mode", "maybe"],
|
"output_schema": {
|
||||||
},
|
"type": "object",
|
||||||
"output_schema": {
|
"properties": {"echoed": {"type": "string"}},
|
||||||
"type": "object",
|
},
|
||||||
"properties": {"echoed": {"type": "string"}},
|
"outcomes": ["ok"],
|
||||||
},
|
}
|
||||||
"outcomes": ["ok"],
|
],
|
||||||
}
|
"nodes": [
|
||||||
],
|
{
|
||||||
"nodes": [
|
"id": "echo",
|
||||||
{
|
"type": "node",
|
||||||
"id": "echo",
|
"node": "echo",
|
||||||
"type": "node",
|
"input": [
|
||||||
"node": "echo",
|
{"target": "message", "path": "input.message"},
|
||||||
"input": [
|
{"target": "mode", "value": "fast"},
|
||||||
{"target": "message", "path": "input.message"},
|
{"target": "maybe", "value": None},
|
||||||
{"target": "mode", "value": "fast"},
|
],
|
||||||
{"target": "maybe", "value": None},
|
"output": [{"source": "echoed", "target": "state.echoed"}],
|
||||||
],
|
}
|
||||||
"output": [{"source": "echoed", "target": "state.echoed"}],
|
],
|
||||||
}
|
"edges": [{"from": "echo", "outcome": "ok", "to": END}],
|
||||||
],
|
}
|
||||||
"edges": [{"from": "echo", "outcome": "ok", "to": END}],
|
)
|
||||||
})
|
|
||||||
run = execute_workflow(
|
run = execute_workflow(
|
||||||
workflow,
|
workflow,
|
||||||
{"message": "hi"},
|
{"message": "hi"},
|
||||||
@@ -128,10 +130,12 @@ def test_missing_nested_node_output_path_fails() -> None:
|
|||||||
def test_root_node_local_paths_map_whole_input_and_output_payloads() -> None:
|
def test_root_node_local_paths_map_whole_input_and_output_payloads() -> None:
|
||||||
workflow = Workflow(
|
workflow = Workflow(
|
||||||
name="root_mapping",
|
name="root_mapping",
|
||||||
input_schema=SchemaRef.model_validate({
|
input_schema=SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {"rates": {"type": "object"}},
|
"type": "object",
|
||||||
}),
|
"properties": {"rates": {"type": "object"}},
|
||||||
|
}
|
||||||
|
),
|
||||||
state_schema=StateSchema.from_field_map({"rates": StateField(type="object")}),
|
state_schema=StateSchema.from_field_map({"rates": StateField(type="object")}),
|
||||||
output_schema=SchemaRef(type="object", properties={}),
|
output_schema=SchemaRef(type="object", properties={}),
|
||||||
node_defs=[
|
node_defs=[
|
||||||
@@ -152,13 +156,15 @@ def test_root_node_local_paths_map_whole_input_and_output_payloads() -> None:
|
|||||||
nodes=[
|
nodes=[
|
||||||
cast(
|
cast(
|
||||||
Any,
|
Any,
|
||||||
NodeUse.model_validate({
|
NodeUse.model_validate(
|
||||||
"id": "force",
|
{
|
||||||
"type": "node",
|
"id": "force",
|
||||||
"node": "force_rates",
|
"type": "node",
|
||||||
"in_map": {"input.rates": "."},
|
"node": "force_rates",
|
||||||
"out_map": {".": "state.rates"},
|
"in_map": {"input.rates": "."},
|
||||||
}),
|
"out_map": {".": "state.rates"},
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
edges=[Edge.model_validate({"from": "force", "outcome": "ok", "to": END})],
|
edges=[Edge.model_validate({"from": "force", "outcome": "ok", "to": END})],
|
||||||
@@ -190,16 +196,20 @@ def test_static_input_values_are_merged_into_node_local_input() -> None:
|
|||||||
node_defs=[
|
node_defs=[
|
||||||
NodeDef(
|
NodeDef(
|
||||||
name="constant",
|
name="constant",
|
||||||
input_schema=SchemaRef.model_validate({
|
input_schema=SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {"value": {"type": "string"}},
|
"type": "object",
|
||||||
"required": ["value"],
|
"properties": {"value": {"type": "string"}},
|
||||||
}),
|
"required": ["value"],
|
||||||
output_schema=SchemaRef.model_validate({
|
}
|
||||||
"type": "object",
|
),
|
||||||
"properties": {"value": {"type": "string"}},
|
output_schema=SchemaRef.model_validate(
|
||||||
"required": ["value"],
|
{
|
||||||
}),
|
"type": "object",
|
||||||
|
"properties": {"value": {"type": "string"}},
|
||||||
|
"required": ["value"],
|
||||||
|
}
|
||||||
|
),
|
||||||
outcomes=["ok"],
|
outcomes=["ok"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -207,13 +217,15 @@ def test_static_input_values_are_merged_into_node_local_input() -> None:
|
|||||||
nodes=[
|
nodes=[
|
||||||
cast(
|
cast(
|
||||||
Any,
|
Any,
|
||||||
NodeUse.model_validate({
|
NodeUse.model_validate(
|
||||||
"id": "constant",
|
{
|
||||||
"type": "node",
|
"id": "constant",
|
||||||
"node": "constant",
|
"type": "node",
|
||||||
"input_values": {"value": "CLICKED"},
|
"node": "constant",
|
||||||
"out_map": {"value": "state.message"},
|
"input_values": {"value": "CLICKED"},
|
||||||
}),
|
"out_map": {"value": "state.message"},
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
edges=[Edge.model_validate({"from": "constant", "outcome": "ok", "to": END})],
|
edges=[Edge.model_validate({"from": "constant", "outcome": "ok", "to": END})],
|
||||||
@@ -232,32 +244,40 @@ def test_static_input_values_are_merged_into_node_local_input() -> None:
|
|||||||
def _nested_mapping_workflow() -> Workflow:
|
def _nested_mapping_workflow() -> Workflow:
|
||||||
return Workflow(
|
return Workflow(
|
||||||
name="nested_mapping",
|
name="nested_mapping",
|
||||||
input_schema=SchemaRef.model_validate({
|
input_schema=SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {
|
"type": "object",
|
||||||
"person": {"type": "object"},
|
"properties": {
|
||||||
"digital": {"type": "object"},
|
"person": {"type": "object"},
|
||||||
},
|
"digital": {"type": "object"},
|
||||||
}),
|
},
|
||||||
state_schema=StateSchema.from_field_map({
|
}
|
||||||
"person": StateField(type="object"),
|
),
|
||||||
"experience": StateField(type="object"),
|
state_schema=StateSchema.from_field_map(
|
||||||
}),
|
{
|
||||||
|
"person": StateField(type="object"),
|
||||||
|
"experience": StateField(type="object"),
|
||||||
|
}
|
||||||
|
),
|
||||||
output_schema=SchemaRef(type="object", properties={}),
|
output_schema=SchemaRef(type="object", properties={}),
|
||||||
node_defs=[
|
node_defs=[
|
||||||
NodeDef(
|
NodeDef(
|
||||||
name="big_tool",
|
name="big_tool",
|
||||||
input_schema=SchemaRef.model_validate({
|
input_schema=SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {"user": {"type": "object"}},
|
"type": "object",
|
||||||
}),
|
"properties": {"user": {"type": "object"}},
|
||||||
output_schema=SchemaRef.model_validate({
|
}
|
||||||
"type": "object",
|
),
|
||||||
"properties": {
|
output_schema=SchemaRef.model_validate(
|
||||||
"user": {"type": "object"},
|
{
|
||||||
"job": {"type": "object"},
|
"type": "object",
|
||||||
},
|
"properties": {
|
||||||
}),
|
"user": {"type": "object"},
|
||||||
|
"job": {"type": "object"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
outcomes=["ok"],
|
outcomes=["ok"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -265,20 +285,22 @@ def _nested_mapping_workflow() -> Workflow:
|
|||||||
nodes=[
|
nodes=[
|
||||||
cast(
|
cast(
|
||||||
Any,
|
Any,
|
||||||
NodeUse.model_validate({
|
NodeUse.model_validate(
|
||||||
"id": "big",
|
{
|
||||||
"type": "node",
|
"id": "big",
|
||||||
"node": "big_tool",
|
"type": "node",
|
||||||
"in_map": {
|
"node": "big_tool",
|
||||||
"input.person.name": "user.name",
|
"in_map": {
|
||||||
"input.digital.email": "user.email",
|
"input.person.name": "user.name",
|
||||||
},
|
"input.digital.email": "user.email",
|
||||||
"out_map": {
|
},
|
||||||
"user.age": "state.person.age",
|
"out_map": {
|
||||||
"user.gender": "state.person.gender",
|
"user.age": "state.person.age",
|
||||||
"job.years": "state.experience.years",
|
"user.gender": "state.person.gender",
|
||||||
},
|
"job.years": "state.experience.years",
|
||||||
}),
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
edges=[Edge.model_validate({"from": "big", "outcome": "ok", "to": END})],
|
edges=[Edge.model_validate({"from": "big", "outcome": "ok", "to": END})],
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ from wf_core.runtime.ops.schemas import validate_payload_against_schema
|
|||||||
|
|
||||||
|
|
||||||
def test_schema_validation_rejects_wrong_property_type() -> None:
|
def test_schema_validation_rejects_wrong_property_type() -> None:
|
||||||
schema = SchemaRef.model_validate({
|
schema = SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {
|
"type": "object",
|
||||||
"name": {"type": "string"},
|
"properties": {
|
||||||
"count": {"type": "integer"},
|
"name": {"type": "string"},
|
||||||
},
|
"count": {"type": "integer"},
|
||||||
"required": ["name", "count"],
|
},
|
||||||
})
|
"required": ["name", "count"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(WorkflowExecutionError, match=r"count.*not of type 'integer'"):
|
with pytest.raises(WorkflowExecutionError, match=r"count.*not of type 'integer'"):
|
||||||
validate_payload_against_schema(
|
validate_payload_against_schema(
|
||||||
@@ -28,17 +30,19 @@ def test_schema_validation_rejects_wrong_property_type() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_schema_validation_rejects_nested_missing_required_field() -> None:
|
def test_schema_validation_rejects_nested_missing_required_field() -> None:
|
||||||
schema = SchemaRef.model_validate({
|
schema = SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {
|
"type": "object",
|
||||||
"profile": {
|
"properties": {
|
||||||
"type": "object",
|
"profile": {
|
||||||
"properties": {"email": {"type": "string"}},
|
"type": "object",
|
||||||
"required": ["email"],
|
"properties": {"email": {"type": "string"}},
|
||||||
}
|
"required": ["email"],
|
||||||
},
|
}
|
||||||
"required": ["profile"],
|
},
|
||||||
})
|
"required": ["profile"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(WorkflowExecutionError, match=r"profile.*email.*required"):
|
with pytest.raises(WorkflowExecutionError, match=r"profile.*email.*required"):
|
||||||
validate_payload_against_schema(
|
validate_payload_against_schema(
|
||||||
@@ -49,31 +53,35 @@ def test_schema_validation_rejects_nested_missing_required_field() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_schema_validation_accepts_valid_payload() -> None:
|
def test_schema_validation_accepts_valid_payload() -> None:
|
||||||
schema = SchemaRef.model_validate({
|
schema = SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {
|
"type": "object",
|
||||||
"tags": {"type": "array", "items": {"type": "string"}},
|
"properties": {
|
||||||
},
|
"tags": {"type": "array", "items": {"type": "string"}},
|
||||||
"required": ["tags"],
|
},
|
||||||
})
|
"required": ["tags"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
validate_payload_against_schema(schema, {"tags": ["a", "b"]}, "node input")
|
validate_payload_against_schema(schema, {"tags": ["a", "b"]}, "node input")
|
||||||
|
|
||||||
|
|
||||||
def test_schema_ref_accepts_and_preserves_schema_with_defs_and_ref() -> None:
|
def test_schema_ref_accepts_and_preserves_schema_with_defs_and_ref() -> None:
|
||||||
schema = SchemaRef.model_validate({
|
schema = SchemaRef.model_validate(
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
{
|
||||||
"$defs": {
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"tag": {
|
"$defs": {
|
||||||
"type": "object",
|
"tag": {
|
||||||
"properties": {"name": {"type": "string"}},
|
"type": "object",
|
||||||
"required": ["name"],
|
"properties": {"name": {"type": "string"}},
|
||||||
}
|
"required": ["name"],
|
||||||
},
|
}
|
||||||
"type": "object",
|
},
|
||||||
"properties": {"tag": {"$ref": "#/$defs/tag"}},
|
"type": "object",
|
||||||
"required": ["tag"],
|
"properties": {"tag": {"$ref": "#/$defs/tag"}},
|
||||||
})
|
"required": ["tag"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dumped = schema.model_dump(mode="json")
|
dumped = schema.model_dump(mode="json")
|
||||||
|
|
||||||
@@ -89,11 +97,13 @@ def test_schema_ref_rejects_invalid_json_schema_shape() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_schema_ref_defaults_to_draft_2020_12_without_schema_keyword() -> None:
|
def test_schema_ref_defaults_to_draft_2020_12_without_schema_keyword() -> None:
|
||||||
schema = SchemaRef.model_validate({
|
schema = SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {"count": {"type": "integer"}},
|
"type": "object",
|
||||||
"required": ["count"],
|
"properties": {"count": {"type": "integer"}},
|
||||||
})
|
"required": ["count"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dumped = schema.model_dump(mode="json")
|
dumped = schema.model_dump(mode="json")
|
||||||
|
|
||||||
@@ -103,11 +113,13 @@ def test_schema_ref_defaults_to_draft_2020_12_without_schema_keyword() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_schema_ref_preserves_extra_json_schema_keywords() -> None:
|
def test_schema_ref_preserves_extra_json_schema_keywords() -> None:
|
||||||
schema = SchemaRef.model_validate({
|
schema = SchemaRef.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {"name": {"type": "string"}},
|
"type": "object",
|
||||||
"additionalProperties": False,
|
"properties": {"name": {"type": "string"}},
|
||||||
})
|
"additionalProperties": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dumped = schema.model_dump(mode="json")
|
dumped = schema.model_dump(mode="json")
|
||||||
|
|
||||||
@@ -125,10 +137,12 @@ def test_schema_ref_dump_omits_none_fields_and_stays_valid_json_schema() -> None
|
|||||||
|
|
||||||
|
|
||||||
def test_state_field_decl_dump_omits_nested_schema_none_fields() -> None:
|
def test_state_field_decl_dump_omits_nested_schema_none_fields() -> None:
|
||||||
field = StateFieldDecl.model_validate({
|
field = StateFieldDecl.model_validate(
|
||||||
"path": "state.person",
|
{
|
||||||
"schema": {"type": "object"},
|
"path": "state.person",
|
||||||
})
|
"schema": {"type": "object"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dumped = field.model_dump(mode="json")
|
dumped = field.model_dump(mode="json")
|
||||||
|
|
||||||
@@ -140,16 +154,18 @@ def test_state_field_decl_dump_omits_nested_schema_none_fields() -> None:
|
|||||||
def test_state_schema_dump_is_valid_json_schema_with_reducer_keyword() -> None:
|
def test_state_schema_dump_is_valid_json_schema_with_reducer_keyword() -> None:
|
||||||
from wf_core import StateSchema
|
from wf_core import StateSchema
|
||||||
|
|
||||||
schema = StateSchema.model_validate({
|
schema = StateSchema.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"properties": {
|
"type": "object",
|
||||||
"count": {
|
"properties": {
|
||||||
"type": "integer",
|
"count": {
|
||||||
"description": "Running count",
|
"type": "integer",
|
||||||
"reducer": "wf.std.add",
|
"description": "Running count",
|
||||||
}
|
"reducer": "wf.std.add",
|
||||||
},
|
}
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
dumped = schema.model_dump(mode="json")
|
dumped = schema.model_dump(mode="json")
|
||||||
assert dumped["type"] == "object"
|
assert dumped["type"] == "object"
|
||||||
@@ -161,22 +177,24 @@ def test_state_schema_dump_is_valid_json_schema_with_reducer_keyword() -> None:
|
|||||||
def test_state_field_validation_schema_preserves_root_defs_for_local_refs() -> None:
|
def test_state_field_validation_schema_preserves_root_defs_for_local_refs() -> None:
|
||||||
from wf_core import StateSchema
|
from wf_core import StateSchema
|
||||||
|
|
||||||
schema = StateSchema.model_validate({
|
schema = StateSchema.model_validate(
|
||||||
"type": "object",
|
{
|
||||||
"$defs": {
|
"type": "object",
|
||||||
"PoolByCategory": {
|
"$defs": {
|
||||||
"type": "object",
|
"PoolByCategory": {
|
||||||
"properties": {"category": {"type": "string"}},
|
"type": "object",
|
||||||
"required": ["category"],
|
"properties": {"category": {"type": "string"}},
|
||||||
}
|
"required": ["category"],
|
||||||
},
|
}
|
||||||
"properties": {
|
},
|
||||||
"current_pools": {
|
"properties": {
|
||||||
"type": "array",
|
"current_pools": {
|
||||||
"items": {"$ref": "#/$defs/PoolByCategory"},
|
"type": "array",
|
||||||
}
|
"items": {"$ref": "#/$defs/PoolByCategory"},
|
||||||
},
|
}
|
||||||
})
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
field_schema = schema.field_map()["current_pools"].validation_schema
|
field_schema = schema.field_map()["current_pools"].validation_schema
|
||||||
|
|
||||||
|
|||||||
Vendored
+8
-6
@@ -26,12 +26,14 @@ async def echo_tool(
|
|||||||
async def resource_link_tool() -> list[mcp_types.ResourceLink]:
|
async def resource_link_tool() -> list[mcp_types.ResourceLink]:
|
||||||
"""Return a link to a fixture resource so proxy URI rewriting is testable."""
|
"""Return a link to a fixture resource so proxy URI rewriting is testable."""
|
||||||
return [
|
return [
|
||||||
mcp_types.ResourceLink.model_validate({
|
mcp_types.ResourceLink.model_validate(
|
||||||
"type": "resource_link",
|
{
|
||||||
"name": "resource.welcome",
|
"type": "resource_link",
|
||||||
"uri": "fixture://docs/welcome",
|
"name": "resource.welcome",
|
||||||
"mimeType": "text/plain",
|
"uri": "fixture://docs/welcome",
|
||||||
})
|
"mimeType": "text/plain",
|
||||||
|
}
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+53
-41
@@ -109,33 +109,37 @@ class RateChange:
|
|||||||
@node(name="force 6* rating")
|
@node(name="force 6* rating")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def r80(r: Rates) -> Rates:
|
def r80(r: Rates) -> Rates:
|
||||||
return Rates.model_validate({
|
return Rates.model_validate(
|
||||||
"rates": {
|
{
|
||||||
"r_1": 0,
|
"rates": {
|
||||||
"r_10": 0,
|
"r_1": 0,
|
||||||
"r_80": r.rates["r_80"],
|
"r_10": 0,
|
||||||
"r_240": r.rates["r_240"],
|
"r_80": r.rates["r_80"],
|
||||||
|
"r_240": r.rates["r_240"],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
@node(name="force banner rating")
|
@node(name="force banner rating")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def r240(_: Nothing) -> Rates:
|
def r240(_: Nothing) -> Rates:
|
||||||
return Rates.model_validate({
|
return Rates.model_validate(
|
||||||
"rates": {"r_1": 0, "r_10": 0, "r_80": 0, "r_240": 1}
|
{"rates": {"r_1": 0, "r_10": 0, "r_80": 0, "r_240": 1}}
|
||||||
})
|
)
|
||||||
|
|
||||||
@node(name="force 5*+ rating")
|
@node(name="force 5*+ rating")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def r10(r: Rates) -> Rates:
|
def r10(r: Rates) -> Rates:
|
||||||
return Rates.model_validate({
|
return Rates.model_validate(
|
||||||
"rates": {
|
{
|
||||||
"r_1": 0,
|
"rates": {
|
||||||
"r_10": r.rates["r_10"],
|
"r_1": 0,
|
||||||
"r_80": r.rates["r_80"],
|
"r_10": r.rates["r_10"],
|
||||||
"r_240": r.rates["r_240"],
|
"r_80": r.rates["r_80"],
|
||||||
|
"r_240": r.rates["r_240"],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
@node(name="buff 6* rating")
|
@node(name="buff 6* rating")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -153,14 +157,16 @@ class RateChange:
|
|||||||
r80 = br["r_80"] * (1 + rpn)
|
r80 = br["r_80"] * (1 + rpn)
|
||||||
r10 = br["r_10"] # use initial rates because i dont know how this works
|
r10 = br["r_10"] # use initial rates because i dont know how this works
|
||||||
r1 = 1 - r240 - r80 - r10
|
r1 = 1 - r240 - r80 - r10
|
||||||
return Rates.model_validate({
|
return Rates.model_validate(
|
||||||
"rates": {
|
{
|
||||||
"r_1": r1,
|
"rates": {
|
||||||
"r_10": r10,
|
"r_1": r1,
|
||||||
"r_80": r80,
|
"r_10": r10,
|
||||||
"r_240": r240,
|
"r_80": r80,
|
||||||
|
"r_240": r240,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
@node(name="reset rating")
|
@node(name="reset rating")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -172,24 +178,28 @@ class CounterUp:
|
|||||||
@node(name="counter 6* reset")
|
@node(name="counter 6* reset")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def c80(_: Nothing) -> Counters:
|
def c80(_: Nothing) -> Counters:
|
||||||
return Counters.model_validate({
|
return Counters.model_validate(
|
||||||
"counter": {
|
{
|
||||||
"c_80": 0,
|
"counter": {
|
||||||
"c_10": 0,
|
"c_80": 0,
|
||||||
},
|
"c_10": 0,
|
||||||
"simple_counter": 0,
|
},
|
||||||
# this is influenced by the add reducer.
|
"simple_counter": 0,
|
||||||
# its top level. it doesnt reset. its a miracle. i hate this.
|
# this is influenced by the add reducer.
|
||||||
})
|
# its top level. it doesnt reset. its a miracle. i hate this.
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@node(name="counter 5* reset")
|
@node(name="counter 5* reset")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def c10(c: Counters) -> Counters:
|
def c10(c: Counters) -> Counters:
|
||||||
c80 = c.counter["c_80"]
|
c80 = c.counter["c_80"]
|
||||||
return Counters.model_validate({
|
return Counters.model_validate(
|
||||||
"counter": {"c_10": 0, "c_80": c80}, # merge with or_!
|
{
|
||||||
"simple_counter": 0,
|
"counter": {"c_10": 0, "c_80": c80}, # merge with or_!
|
||||||
})
|
"simple_counter": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@node(name="counting up")
|
@node(name="counting up")
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -228,10 +238,12 @@ def roll(state: CurrentPools) -> ThisStorage:
|
|||||||
r = state.current_pools
|
r = state.current_pools
|
||||||
(t,) = random.choices(r, weights=[*map(lambda p: p["rates"], r)])
|
(t,) = random.choices(r, weights=[*map(lambda p: p["rates"], r)])
|
||||||
this = Entity(category=t["category"], name=random.choice(t["pool"]))
|
this = Entity(category=t["category"], name=random.choice(t["pool"]))
|
||||||
return ThisStorage.model_validate({
|
return ThisStorage.model_validate(
|
||||||
"this": this,
|
{
|
||||||
"storage": [this], # I NEED MERGE
|
"this": this,
|
||||||
})
|
"storage": [this], # I NEED MERGE
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@node(outcomes=("240", "80", "10", "1")) # missed this! good job.
|
@node(outcomes=("240", "80", "10", "1")) # missed this! good job.
|
||||||
|
|||||||
@@ -72,15 +72,17 @@ def test():
|
|||||||
assert d.status == RunStatus.COMPLETED, "oops"
|
assert d.status == RunStatus.COMPLETED, "oops"
|
||||||
state = State.model_validate(d.state)
|
state = State.model_validate(d.state)
|
||||||
pprint(state.storage)
|
pprint(state.storage)
|
||||||
pprint([
|
pprint(
|
||||||
t
|
[
|
||||||
for t in d.trace
|
t
|
||||||
if t.node_id
|
for t in d.trace
|
||||||
in (
|
if t.node_id
|
||||||
"counter_up",
|
in (
|
||||||
"tick",
|
"counter_up",
|
||||||
)
|
"tick",
|
||||||
])
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
pprint(d.state)
|
pprint(d.state)
|
||||||
assert any(
|
assert any(
|
||||||
i["name"] in context["pool"]["n_240"]
|
i["name"] in context["pool"]["n_240"]
|
||||||
|
|||||||
@@ -84,13 +84,15 @@ class FakeManager:
|
|||||||
metadata: dict[str, Any] | None = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
enabled: bool = True,
|
enabled: bool = True,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
self.added.append({
|
self.added.append(
|
||||||
"connection_id": connection_id,
|
{
|
||||||
"server": server,
|
"connection_id": connection_id,
|
||||||
"account": account,
|
"server": server,
|
||||||
"metadata": metadata,
|
"account": account,
|
||||||
"enabled": enabled,
|
"metadata": metadata,
|
||||||
})
|
"enabled": enabled,
|
||||||
|
}
|
||||||
|
)
|
||||||
return {"action": "add_connection", "ok": True}
|
return {"action": "add_connection", "ok": True}
|
||||||
|
|
||||||
def update_connection(
|
def update_connection(
|
||||||
|
|||||||
+78
-64
@@ -14,29 +14,33 @@ 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(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
"store_root": ".wf_mcp_store",
|
{
|
||||||
"connections": [
|
"store_root": ".wf_mcp_store",
|
||||||
{
|
"connections": [
|
||||||
"id": "demo.personal",
|
{
|
||||||
"server": "demo",
|
"id": "demo.personal",
|
||||||
"account": "personal",
|
"server": "demo",
|
||||||
}
|
"account": "personal",
|
||||||
],
|
}
|
||||||
}),
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_build_parser_accepts_serve_transport() -> None:
|
def test_build_parser_accepts_serve_transport() -> None:
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args([
|
args = parser.parse_args(
|
||||||
"--config",
|
[
|
||||||
"wf_mcp.config.json",
|
"--config",
|
||||||
"serve",
|
"wf_mcp.config.json",
|
||||||
"--transport",
|
"serve",
|
||||||
"streamable_http",
|
"--transport",
|
||||||
])
|
"streamable_http",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
assert args.command == "serve"
|
assert args.command == "serve"
|
||||||
assert args.transport == "streamable_http"
|
assert args.transport == "streamable_http"
|
||||||
@@ -47,15 +51,17 @@ def test_build_parser_accepts_serve_transport() -> None:
|
|||||||
|
|
||||||
def test_build_parser_accepts_proxy_compatibility_flags() -> None:
|
def test_build_parser_accepts_proxy_compatibility_flags() -> None:
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args([
|
args = parser.parse_args(
|
||||||
"--config",
|
[
|
||||||
"wf_mcp.config.json",
|
"--config",
|
||||||
"serve",
|
"wf_mcp.config.json",
|
||||||
"--resources-as-tools",
|
"serve",
|
||||||
"--prompts-as-tools",
|
"--resources-as-tools",
|
||||||
"--search-tools",
|
"--prompts-as-tools",
|
||||||
"--safe-tool-names",
|
"--search-tools",
|
||||||
])
|
"--safe-tool-names",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
assert args.command == "serve"
|
assert args.command == "serve"
|
||||||
assert args.resources_as_tools is True
|
assert args.resources_as_tools is True
|
||||||
@@ -68,23 +74,27 @@ def test_build_parser_rejects_legacy_mode_flag() -> None:
|
|||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
|
|
||||||
with pytest.raises(SystemExit):
|
with pytest.raises(SystemExit):
|
||||||
parser.parse_args([
|
parser.parse_args(
|
||||||
"--config",
|
[
|
||||||
"wf_mcp.config.json",
|
"--config",
|
||||||
"serve",
|
"wf_mcp.config.json",
|
||||||
"--mode",
|
"serve",
|
||||||
"unified",
|
"--mode",
|
||||||
])
|
"unified",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_build_parser_accepts_no_admin_tools_flag() -> None:
|
def test_build_parser_accepts_no_admin_tools_flag() -> None:
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args([
|
args = parser.parse_args(
|
||||||
"--config",
|
[
|
||||||
"wf_mcp.config.json",
|
"--config",
|
||||||
"serve",
|
"wf_mcp.config.json",
|
||||||
"--no-admin-tools",
|
"serve",
|
||||||
])
|
"--no-admin-tools",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
assert args.command == "serve"
|
assert args.command == "serve"
|
||||||
assert args.admin_tools is False
|
assert args.admin_tools is False
|
||||||
@@ -154,21 +164,23 @@ def test_load_broker_config_normalizes_typed_stdio_metadata() -> None:
|
|||||||
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(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
"store_root": ".wf_mcp_store",
|
{
|
||||||
"connections": [
|
"store_root": ".wf_mcp_store",
|
||||||
{
|
"connections": [
|
||||||
"id": "demo.personal",
|
{
|
||||||
"server": "demo",
|
"id": "demo.personal",
|
||||||
"account": "personal",
|
"server": "demo",
|
||||||
"metadata": {
|
"account": "personal",
|
||||||
"command": "python",
|
"metadata": {
|
||||||
"args": ["server.py"],
|
"command": "python",
|
||||||
"env": {"TOKEN": "secret"},
|
"args": ["server.py"],
|
||||||
},
|
"env": {"TOKEN": "secret"},
|
||||||
}
|
},
|
||||||
],
|
}
|
||||||
}),
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -188,16 +200,18 @@ def test_load_broker_config_rejects_bad_metadata_shape() -> None:
|
|||||||
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(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
"connections": [
|
{
|
||||||
{
|
"connections": [
|
||||||
"id": "demo.personal",
|
{
|
||||||
"server": "demo",
|
"id": "demo.personal",
|
||||||
"account": "personal",
|
"server": "demo",
|
||||||
"metadata": {"transport": "stdio", "args": "server.py"},
|
"account": "personal",
|
||||||
}
|
"metadata": {"transport": "stdio", "args": "server.py"},
|
||||||
],
|
}
|
||||||
}),
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,7 @@ def test_mcp_workflow_surface_example_runs_happy_path(tmp_path) -> None:
|
|||||||
assert payload["diagnostics"] == []
|
assert payload["diagnostics"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_mcp_wrapper_authoring_flow_example_creates_and_calls_wrapper(tmp_path) -> (
|
def test_mcp_wrapper_authoring_flow_example_creates_and_calls_wrapper(tmp_path) -> None:
|
||||||
None
|
|
||||||
):
|
|
||||||
payload = asyncio.run(author_echo_wrapper_from_capability(tmp_path))
|
payload = asyncio.run(author_echo_wrapper_from_capability(tmp_path))
|
||||||
|
|
||||||
assert payload["inspected_hints"]["capability_name"] == "demo.personal.echo_tool"
|
assert payload["inspected_hints"]["capability_name"] == "demo.personal.echo_tool"
|
||||||
|
|||||||
+31
-23
@@ -389,17 +389,19 @@ def test_proxy_admin_tools_mutate_config_file() -> None:
|
|||||||
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(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
"store_root": ".wf_mcp_store",
|
{
|
||||||
"connections": [
|
"store_root": ".wf_mcp_store",
|
||||||
{
|
"connections": [
|
||||||
"id": "fixture.personal",
|
{
|
||||||
"server": "fixture",
|
"id": "fixture.personal",
|
||||||
"account": "personal",
|
"server": "fixture",
|
||||||
"enabled": False,
|
"account": "personal",
|
||||||
}
|
"enabled": False,
|
||||||
],
|
}
|
||||||
}),
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = load_broker_config(config_path)
|
config = load_broker_config(config_path)
|
||||||
@@ -484,10 +486,12 @@ def test_proxy_admin_reload_remounts_connections() -> None:
|
|||||||
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(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
"store_root": ".wf_mcp_store",
|
{
|
||||||
"connections": [],
|
"store_root": ".wf_mcp_store",
|
||||||
}),
|
"connections": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = load_broker_config(config_path)
|
config = load_broker_config(config_path)
|
||||||
@@ -544,10 +548,12 @@ def test_proxy_admin_reload_sends_list_changed_notifications() -> None:
|
|||||||
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(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
"store_root": ".wf_mcp_store",
|
{
|
||||||
"connections": [],
|
"store_root": ".wf_mcp_store",
|
||||||
}),
|
"connections": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = load_broker_config(config_path)
|
config = load_broker_config(config_path)
|
||||||
@@ -576,10 +582,12 @@ def test_proxy_config_mutation_does_not_notify_before_reload() -> None:
|
|||||||
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(
|
||||||
json.dumps({
|
json.dumps(
|
||||||
"store_root": ".wf_mcp_store",
|
{
|
||||||
"connections": [],
|
"store_root": ".wf_mcp_store",
|
||||||
}),
|
"connections": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
config = load_broker_config(config_path)
|
config = load_broker_config(config_path)
|
||||||
|
|||||||
@@ -65,9 +65,11 @@ def test_rewrites_resource_links_inside_call_tool_result() -> None:
|
|||||||
|
|
||||||
def _resource_link(uri: str) -> mcp_types.ResourceLink:
|
def _resource_link(uri: str) -> mcp_types.ResourceLink:
|
||||||
"""Build ResourceLink through validation because Pydantic accepts URI strings."""
|
"""Build ResourceLink through validation because Pydantic accepts URI strings."""
|
||||||
return mcp_types.ResourceLink.model_validate({
|
return mcp_types.ResourceLink.model_validate(
|
||||||
"type": "resource_link",
|
{
|
||||||
"name": "dynamic-text",
|
"type": "resource_link",
|
||||||
"uri": uri,
|
"name": "dynamic-text",
|
||||||
"mimeType": "text/plain",
|
"uri": uri,
|
||||||
})
|
"mimeType": "text/plain",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -133,13 +133,15 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
assert "workspace_id" in create_workspace_schema["properties"]
|
assert "workspace_id" in create_workspace_schema["properties"]
|
||||||
assert "revision" in create_workspace_schema["properties"]
|
assert "revision" in create_workspace_schema["properties"]
|
||||||
list_sources_schema = tools_by_name["wf.admin.list_sources"].inputSchema
|
list_sources_schema = tools_by_name["wf.admin.list_sources"].inputSchema
|
||||||
assert "inspect_source" in list_sources_schema["properties"]["limit"][
|
assert (
|
||||||
"description"
|
"inspect_source"
|
||||||
]
|
in list_sources_schema["properties"]["limit"]["description"]
|
||||||
|
)
|
||||||
inspect_source_schema = tools_by_name["wf.admin.inspect_source"].inputSchema
|
inspect_source_schema = tools_by_name["wf.admin.inspect_source"].inputSchema
|
||||||
assert "Exact source id" in inspect_source_schema["properties"][
|
assert (
|
||||||
"source_id"
|
"Exact source id"
|
||||||
]["description"]
|
in inspect_source_schema["properties"]["source_id"]["description"]
|
||||||
|
)
|
||||||
minimal_workspace_input = tools_by_name[
|
minimal_workspace_input = tools_by_name[
|
||||||
"wf.workflow.create_minimal_draft_workspace"
|
"wf.workflow.create_minimal_draft_workspace"
|
||||||
].inputSchema
|
].inputSchema
|
||||||
@@ -399,9 +401,7 @@ def test_workflow_tools_have_human_metadata() -> None:
|
|||||||
assert run_deployment.title == "Run Workflow Deployment"
|
assert run_deployment.title == "Run Workflow Deployment"
|
||||||
assert "deployment_id" in (run_deployment.description or "")
|
assert "deployment_id" in (run_deployment.description or "")
|
||||||
assert "trace_range" in run_deployment.inputSchema["properties"]
|
assert "trace_range" in run_deployment.inputSchema["properties"]
|
||||||
trace_range_schema = run_deployment.inputSchema["properties"][
|
trace_range_schema = run_deployment.inputSchema["properties"]["trace_range"]
|
||||||
"trace_range"
|
|
||||||
]
|
|
||||||
assert "Debug traces" in trace_range_schema.get("description", "")
|
assert "Debug traces" in trace_range_schema.get("description", "")
|
||||||
assert "null" in [
|
assert "null" in [
|
||||||
option.get("type") for option in trace_range_schema["anyOf"]
|
option.get("type") for option in trace_range_schema["anyOf"]
|
||||||
|
|||||||
@@ -284,9 +284,7 @@ def test_workflow_surface_lists_compact_deployment_summaries_and_inspects_detail
|
|||||||
artifact_store.save_deployment(deployment)
|
artifact_store.save_deployment(deployment)
|
||||||
|
|
||||||
listed = asyncio.run(handlers.list_deployments())
|
listed = asyncio.run(handlers.list_deployments())
|
||||||
inspected = asyncio.run(
|
inspected = asyncio.run(handlers.inspect_deployment(deployment_id="echo.personal"))
|
||||||
handlers.inspect_deployment(deployment_id="echo.personal")
|
|
||||||
)
|
|
||||||
|
|
||||||
assert listed["deployments"][0]["id"] == "echo.personal"
|
assert listed["deployments"][0]["id"] == "echo.personal"
|
||||||
assert listed["deployments"][0]["binding_count"] == 1
|
assert listed["deployments"][0]["binding_count"] == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user