This commit is contained in:
lda
2026-05-22 00:58:53 +07:00 Verified
parent 617eb90ed9
commit ef3434fc64
22 changed files with 623 additions and 516 deletions
+4 -2
View File
@@ -6,7 +6,8 @@ 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", "name": "raw_canonical_echo",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
@@ -60,7 +61,8 @@ def build_raw_canonical_workflow() -> Workflow:
} }
], ],
"edges": [{"from": "format", "outcome": "ok", "to": END}], "edges": [{"from": "format", "outcome": "ok", "to": END}],
}) }
)
def build_raw_canonical_registry(): def build_raw_canonical_registry():
+4 -2
View File
@@ -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 capability.logical_source
for capability in artifact.required_capability_map().values() 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,
+7 -3
View File
@@ -319,7 +319,8 @@ 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, "connection_id": connection.id,
"server": connection.server, "server": connection.server,
"account": connection.account, "account": connection.account,
@@ -332,9 +333,12 @@ class WfMcpService:
if snapshot is None if snapshot is None
else snapshot.max_age_seconds, else snapshot.max_age_seconds,
"node_count": 0 if snapshot is None else len(snapshot.nodes), "node_count": 0 if snapshot is None else len(snapshot.nodes),
"resource_count": 0 if snapshot is None else len(snapshot.resources), "resource_count": 0
if snapshot is None
else len(snapshot.resources),
"prompt_count": 0 if snapshot is None else len(snapshot.prompts), "prompt_count": 0 if snapshot is None else len(snapshot.prompts),
}) }
)
return statuses return statuses
def list_resources( def list_resources(
+19 -9
View File
@@ -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, "connection_id": target_id,
"refreshed": snapshot is not None, "refreshed": snapshot is not None,
"node_count": 0 if snapshot is None else len(snapshot.nodes), "node_count": 0 if snapshot is None else len(snapshot.nodes),
"resource_count": 0 if snapshot is None else len(snapshot.resources), "resource_count": 0
if snapshot is None
else len(snapshot.resources),
"prompt_count": 0 if snapshot is None else len(snapshot.prompts), "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, "connection_id": target_id,
"refreshed": False, "refreshed": False,
"error_type": type(exc).__name__, "error_type": type(exc).__name__,
"error": str(exc), "error": str(exc),
}) }
)
return results return results
@@ -132,7 +138,8 @@ 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, "id": connection.id,
"server": connection.server, "server": connection.server,
@@ -141,7 +148,8 @@ def main(argv: list[str] | None = None) -> int:
"metadata": connection.metadata, "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, "results": results,
"catalog": service.get_catalog().as_payload(), "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
+4 -2
View File
@@ -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": { "mcpServers": {
connection.id: connection_to_fastmcp_server_config(connection) connection.id: connection_to_fastmcp_server_config(connection)
for connection in config.connections for connection in config.connections
if connection.enabled 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(), **tool.model_dump(),
"parent_tool": tool, "parent_tool": tool,
"rewrite_uri": rewrite_uri, "rewrite_uri": rewrite_uri,
}) }
)
class ResourceLinkNamespace(Transform): class ResourceLinkNamespace(Transform):
+1 -3
View File
@@ -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
), ),
+14 -8
View File
@@ -126,7 +126,8 @@ 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", "type": "object",
"properties": { "properties": {
"person": { "person": {
@@ -135,7 +136,8 @@ def test_output_bindings_validate_declared_parent_schema_before_mutation() -> No
"additionalProperties": False, "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", "id": "rename",
"type": "node", "type": "node",
"node": "rename", "node": "rename",
"output": [{"source": "person.name", "target": "state.person.name"}], "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})],
) )
+9 -12
View File
@@ -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"]
+12 -6
View File
@@ -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", "type": "object",
"properties": {"person": {"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", "type": "object",
"properties": {"user": {"type": "object"}}, "properties": {"user": {"type": "object"}},
}), }
output_schema=SchemaRef.model_validate({ ),
output_schema=SchemaRef.model_validate(
{
"type": "object", "type": "object",
"properties": {"user": {"type": "object"}}, "properties": {"user": {"type": "object"}},
}), }
),
outcomes=["ok"], outcomes=["ok"],
) )
], ],
+44 -22
View File
@@ -19,7 +19,8 @@ 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", "name": "canonical",
"input_schema": { "input_schema": {
"type": "object", "type": "object",
@@ -64,7 +65,8 @@ def test_canonical_bindings_resolve_input_values_paths_and_explicit_null() -> No
} }
], ],
"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", "type": "object",
"properties": {"rates": {"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", "id": "force",
"type": "node", "type": "node",
"node": "force_rates", "node": "force_rates",
"in_map": {"input.rates": "."}, "in_map": {"input.rates": "."},
"out_map": {".": "state.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", "type": "object",
"properties": {"value": {"type": "string"}}, "properties": {"value": {"type": "string"}},
"required": ["value"], "required": ["value"],
}), }
output_schema=SchemaRef.model_validate({ ),
output_schema=SchemaRef.model_validate(
{
"type": "object", "type": "object",
"properties": {"value": {"type": "string"}}, "properties": {"value": {"type": "string"}},
"required": ["value"], "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", "id": "constant",
"type": "node", "type": "node",
"node": "constant", "node": "constant",
"input_values": {"value": "CLICKED"}, "input_values": {"value": "CLICKED"},
"out_map": {"value": "state.message"}, "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", "type": "object",
"properties": { "properties": {
"person": {"type": "object"}, "person": {"type": "object"},
"digital": {"type": "object"}, "digital": {"type": "object"},
}, },
}), }
state_schema=StateSchema.from_field_map({ ),
state_schema=StateSchema.from_field_map(
{
"person": StateField(type="object"), "person": StateField(type="object"),
"experience": 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", "type": "object",
"properties": {"user": {"type": "object"}}, "properties": {"user": {"type": "object"}},
}), }
output_schema=SchemaRef.model_validate({ ),
output_schema=SchemaRef.model_validate(
{
"type": "object", "type": "object",
"properties": { "properties": {
"user": {"type": "object"}, "user": {"type": "object"},
"job": {"type": "object"}, "job": {"type": "object"},
}, },
}), }
),
outcomes=["ok"], outcomes=["ok"],
) )
], ],
@@ -265,7 +285,8 @@ def _nested_mapping_workflow() -> Workflow:
nodes=[ nodes=[
cast( cast(
Any, Any,
NodeUse.model_validate({ NodeUse.model_validate(
{
"id": "big", "id": "big",
"type": "node", "type": "node",
"node": "big_tool", "node": "big_tool",
@@ -278,7 +299,8 @@ def _nested_mapping_workflow() -> Workflow:
"user.gender": "state.person.gender", "user.gender": "state.person.gender",
"job.years": "state.experience.years", "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})],
+36 -18
View File
@@ -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", "type": "object",
"properties": { "properties": {
"name": {"type": "string"}, "name": {"type": "string"},
"count": {"type": "integer"}, "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,7 +30,8 @@ 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", "type": "object",
"properties": { "properties": {
"profile": { "profile": {
@@ -38,7 +41,8 @@ def test_schema_validation_rejects_nested_missing_required_field() -> None:
} }
}, },
"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,19 +53,22 @@ 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", "type": "object",
"properties": { "properties": {
"tags": {"type": "array", "items": {"type": "string"}}, "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", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": { "$defs": {
"tag": { "tag": {
@@ -73,7 +80,8 @@ def test_schema_ref_accepts_and_preserves_schema_with_defs_and_ref() -> None:
"type": "object", "type": "object",
"properties": {"tag": {"$ref": "#/$defs/tag"}}, "properties": {"tag": {"$ref": "#/$defs/tag"}},
"required": ["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", "type": "object",
"properties": {"count": {"type": "integer"}}, "properties": {"count": {"type": "integer"}},
"required": ["count"], "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", "type": "object",
"properties": {"name": {"type": "string"}}, "properties": {"name": {"type": "string"}},
"additionalProperties": False, "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", "path": "state.person",
"schema": {"type": "object"}, "schema": {"type": "object"},
}) }
)
dumped = field.model_dump(mode="json") dumped = field.model_dump(mode="json")
@@ -140,7 +154,8 @@ 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", "type": "object",
"properties": { "properties": {
"count": { "count": {
@@ -149,7 +164,8 @@ def test_state_schema_dump_is_valid_json_schema_with_reducer_keyword() -> None:
"reducer": "wf.std.add", "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,7 +177,8 @@ 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", "type": "object",
"$defs": { "$defs": {
"PoolByCategory": { "PoolByCategory": {
@@ -176,7 +193,8 @@ def test_state_field_validation_schema_preserves_root_defs_for_local_refs() -> N
"items": {"$ref": "#/$defs/PoolByCategory"}, "items": {"$ref": "#/$defs/PoolByCategory"},
} }
}, },
}) }
)
field_schema = schema.field_map()["current_pools"].validation_schema field_schema = schema.field_map()["current_pools"].validation_schema
+4 -2
View File
@@ -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", "type": "resource_link",
"name": "resource.welcome", "name": "resource.welcome",
"uri": "fixture://docs/welcome", "uri": "fixture://docs/welcome",
"mimeType": "text/plain", "mimeType": "text/plain",
}) }
)
] ]
+27 -15
View File
@@ -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": { "rates": {
"r_1": 0, "r_1": 0,
"r_10": 0, "r_10": 0,
"r_80": r.rates["r_80"], "r_80": r.rates["r_80"],
"r_240": r.rates["r_240"], "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": { "rates": {
"r_1": 0, "r_1": 0,
"r_10": r.rates["r_10"], "r_10": r.rates["r_10"],
"r_80": r.rates["r_80"], "r_80": r.rates["r_80"],
"r_240": r.rates["r_240"], "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": { "rates": {
"r_1": r1, "r_1": r1,
"r_10": r10, "r_10": r10,
"r_80": r80, "r_80": r80,
"r_240": r240, "r_240": r240,
} }
}) }
)
@node(name="reset rating") @node(name="reset rating")
@staticmethod @staticmethod
@@ -172,7 +178,8 @@ 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": { "counter": {
"c_80": 0, "c_80": 0,
"c_10": 0, "c_10": 0,
@@ -180,16 +187,19 @@ class CounterUp:
"simple_counter": 0, "simple_counter": 0,
# this is influenced by the add reducer. # this is influenced by the add reducer.
# its top level. it doesnt reset. its a miracle. i hate this. # 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_! "counter": {"c_10": 0, "c_80": c80}, # merge with or_!
"simple_counter": 0, "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, "this": this,
"storage": [this], # I NEED MERGE "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.
+4 -2
View File
@@ -72,7 +72,8 @@ 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 t
for t in d.trace for t in d.trace
if t.node_id if t.node_id
@@ -80,7 +81,8 @@ def test():
"counter_up", "counter_up",
"tick", "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"]
+4 -2
View File
@@ -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, "connection_id": connection_id,
"server": server, "server": server,
"account": account, "account": account,
"metadata": metadata, "metadata": metadata,
"enabled": enabled, "enabled": enabled,
}) }
)
return {"action": "add_connection", "ok": True} return {"action": "add_connection", "ok": True}
def update_connection( def update_connection(
+28 -14
View File
@@ -14,7 +14,8 @@ 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", "store_root": ".wf_mcp_store",
"connections": [ "connections": [
{ {
@@ -23,20 +24,23 @@ def _write_config(path: Path) -> None:
"account": "personal", "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", "--config",
"wf_mcp.config.json", "wf_mcp.config.json",
"serve", "serve",
"--transport", "--transport",
"streamable_http", "streamable_http",
]) ]
)
assert args.command == "serve" assert args.command == "serve"
assert args.transport == "streamable_http" assert args.transport == "streamable_http"
@@ -47,7 +51,8 @@ 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", "--config",
"wf_mcp.config.json", "wf_mcp.config.json",
"serve", "serve",
@@ -55,7 +60,8 @@ def test_build_parser_accepts_proxy_compatibility_flags() -> None:
"--prompts-as-tools", "--prompts-as-tools",
"--search-tools", "--search-tools",
"--safe-tool-names", "--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", "--config",
"wf_mcp.config.json", "wf_mcp.config.json",
"serve", "serve",
"--mode", "--mode",
"unified", "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", "--config",
"wf_mcp.config.json", "wf_mcp.config.json",
"serve", "serve",
"--no-admin-tools", "--no-admin-tools",
]) ]
)
assert args.command == "serve" assert args.command == "serve"
assert args.admin_tools is False assert args.admin_tools is False
@@ -154,7 +164,8 @@ 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", "store_root": ".wf_mcp_store",
"connections": [ "connections": [
{ {
@@ -168,7 +179,8 @@ def test_load_broker_config_normalizes_typed_stdio_metadata() -> None:
}, },
} }
], ],
}), }
),
encoding="utf-8", encoding="utf-8",
) )
@@ -188,7 +200,8 @@ 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", "id": "demo.personal",
@@ -197,7 +210,8 @@ def test_load_broker_config_rejects_bad_metadata_shape() -> None:
"metadata": {"transport": "stdio", "args": "server.py"}, "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"
+16 -8
View File
@@ -389,7 +389,8 @@ 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", "store_root": ".wf_mcp_store",
"connections": [ "connections": [
{ {
@@ -399,7 +400,8 @@ def test_proxy_admin_tools_mutate_config_file() -> None:
"enabled": False, "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", "store_root": ".wf_mcp_store",
"connections": [], "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", "store_root": ".wf_mcp_store",
"connections": [], "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", "store_root": ".wf_mcp_store",
"connections": [], "connections": [],
}), }
),
encoding="utf-8", encoding="utf-8",
) )
config = load_broker_config(config_path) config = load_broker_config(config_path)
+4 -2
View File
@@ -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", "type": "resource_link",
"name": "dynamic-text", "name": "dynamic-text",
"uri": uri, "uri": uri,
"mimeType": "text/plain", "mimeType": "text/plain",
}) }
)
+9 -9
View File
@@ -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"]
+1 -3
View File
@@ -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