chore: finish compatibility cleanup
This commit is contained in:
@@ -46,46 +46,30 @@ passes with AnyIO 4.14.1 or newer, and the full suite remains clean.
|
||||
| `ddcdf648` from PR #4363 | 4.14.1 | Pass |
|
||||
| `main` at `de521e65` | 4.14.1 | Pass |
|
||||
|
||||
## Typer Command Callback Errors
|
||||
## Typer Vendored Click Error Boundary
|
||||
|
||||
With Typer 0.26.8 and Click 8.4.2, a nested Typer command that raises
|
||||
`click.ClickException` exits with code 1 under `typer.testing.CliRunner`, but
|
||||
both captured output streams are empty. The unformatted `ClickException`
|
||||
remains on `result.exception`. Single-command applications behave the same way,
|
||||
and direct invocation renders a traceback instead of a concise error.
|
||||
Typer 0.26.0 intentionally vendored Click and stopped supporting direct use of
|
||||
Click-specific functionality. External `click.ClickException` instances are
|
||||
therefore distinct from Typer's internal exception classes and bypass Typer's
|
||||
concise error formatter.
|
||||
|
||||
The CLI therefore writes its concise remote-operation error explicitly to
|
||||
stderr and raises `typer.Exit(1)` instead of relying on standalone Click
|
||||
exception formatting. This keeps real CLI and test-runner behavior aligned.
|
||||
|
||||
Minimal reproducer:
|
||||
The CLI uses Typer's supported public API instead:
|
||||
|
||||
```python
|
||||
import click
|
||||
import typer
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def fail() -> None:
|
||||
raise click.ClickException("broken")
|
||||
|
||||
|
||||
result = CliRunner().invoke(app)
|
||||
assert result.exit_code == 1
|
||||
assert result.stderr == "" # Expected to contain "Error: broken".
|
||||
assert isinstance(result.exception, click.ClickException)
|
||||
typer.echo("Error: broken", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
```
|
||||
|
||||
This regression is tracked as Typer issue
|
||||
[#1867](https://github.com/fastapi/typer/issues/1867). The repository copy of
|
||||
the verified issue report is
|
||||
[`2026-06-29-typer-click-exception-regression.md`](../superpowers/research/2026-06-29-typer-click-exception-regression.md).
|
||||
Typer 0.24.2 and 0.25.0 format the exception normally. Vendoring Click in Typer
|
||||
0.26.0 introduced the exception-class identity mismatch; Typer 0.26.8 and
|
||||
current `master` at `b210c0e2` reproduce the failure.
|
||||
Typer issue [#1867](https://github.com/fastapi/typer/issues/1867) now tracks the
|
||||
narrower feature request for a public general-purpose Typer CLI error. The
|
||||
repository copy of the request is
|
||||
[`2026-06-29-typer-public-cli-error-feature.md`](../superpowers/research/2026-06-29-typer-public-cli-error-feature.md).
|
||||
|
||||
## Separate Warning Backlog
|
||||
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
### Description
|
||||
|
||||
After Typer vendored Click in 0.26.0, exceptions imported from the external
|
||||
`click` package are no longer handled as normal Typer CLI errors.
|
||||
|
||||
A real invocation renders a Rich traceback ending in
|
||||
`ClickException: broken`. Under `typer.testing.CliRunner`, both stdout and
|
||||
stderr are empty and the raw `click.ClickException` remains in
|
||||
`result.exception`.
|
||||
|
||||
Typer 0.25.0 renders the expected concise error panel and returns `SystemExit`.
|
||||
|
||||
### Root Cause
|
||||
|
||||
Typer commit `1829d73` vendored Click as `typer._click`. Typer's `_main()`
|
||||
catches `typer._click.exceptions.ClickException`, but an application that
|
||||
raises `click.exceptions.ClickException` raises a different class:
|
||||
|
||||
```python
|
||||
import click
|
||||
import typer._click
|
||||
|
||||
assert click.ClickException is not typer._click.ClickException
|
||||
```
|
||||
|
||||
The exception therefore bypasses Typer's Click-exception handler. This affects
|
||||
the external Click variants of `ClickException`, `UsageError`, `BadParameter`,
|
||||
and other subclasses.
|
||||
|
||||
Typer publicly re-exports some vendored exception types, including
|
||||
`typer.BadParameter` and `typer.Exit`, but it does not currently expose a public
|
||||
`typer.ClickException` or `typer.UsageError` equivalent.
|
||||
|
||||
### Minimal Reproduction
|
||||
|
||||
```python
|
||||
import click
|
||||
import typer
|
||||
from typer.testing import CliRunner
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def fail() -> None:
|
||||
raise click.ClickException("broken")
|
||||
|
||||
|
||||
result = CliRunner().invoke(app)
|
||||
print("exit:", result.exit_code)
|
||||
print("stdout:", repr(result.stdout))
|
||||
print("stderr:", repr(result.stderr))
|
||||
print("exception:", type(result.exception).__name__)
|
||||
```
|
||||
|
||||
The same behavior occurs in nested Typer command groups.
|
||||
|
||||
### Version Matrix
|
||||
|
||||
Tested with Click 8.4.2 unless noted otherwise:
|
||||
|
||||
| Typer | Result |
|
||||
| --- | --- |
|
||||
| 0.24.2 | Concise error output; `SystemExit` |
|
||||
| 0.25.0 | Concise error output; `SystemExit` |
|
||||
| 0.26.0 | Empty runner output; raw `ClickException` |
|
||||
| 0.26.8 with Click 8.1.7 | Empty runner output; raw `ClickException` |
|
||||
| 0.26.8 with Click 8.4.2 | Empty runner output; raw `ClickException` |
|
||||
| `master` at `b210c0e2` | Empty runner output; raw `ClickException` |
|
||||
|
||||
Test environment:
|
||||
|
||||
- Python 3.14.3
|
||||
- Windows 11
|
||||
- Click 8.1.7 and 8.4.2
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
Typer should provide a public way to raise its concise general-purpose CLI
|
||||
exception after vendoring Click. Possible resolutions include:
|
||||
|
||||
- re-exporting the vendored `ClickException` and `UsageError` classes from the
|
||||
`typer` namespace;
|
||||
- preserving compatibility with exceptions imported from external Click; or
|
||||
- documenting that external Click exceptions are no longer compatible and
|
||||
identifying their supported Typer replacements.
|
||||
|
||||
### Workaround
|
||||
|
||||
Applications can write the message explicitly and raise `typer.Exit(1)`:
|
||||
|
||||
```python
|
||||
typer.echo("Error: something went wrong", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
```
|
||||
|
||||
Importing from `typer._click` also works, but relies on a private API.
|
||||
@@ -0,0 +1,74 @@
|
||||
### Feature Request
|
||||
|
||||
Expose a public, general-purpose Typer exception for concise user-facing CLI
|
||||
errors after Click vendoring.
|
||||
|
||||
Typer 0.26.0 intentionally vendored Click and no longer supports using Click
|
||||
directly. This is documented in the 0.26.0 breaking changes and the Vendored
|
||||
Click guide. This request is not asking to restore compatibility with exception
|
||||
classes imported from the external `click` package.
|
||||
|
||||
### Current API Gap
|
||||
|
||||
Typer publicly re-exports several vendored exception types, including
|
||||
`typer.BadParameter`, `typer.Abort`, and `typer.Exit`, but it does not expose a
|
||||
general-purpose `typer.ClickException` or `typer.UsageError` equivalent.
|
||||
|
||||
Before 0.26.0, an application could raise `click.ClickException("broken")` to
|
||||
produce Typer's concise error panel and exit with code 1. After vendoring, the
|
||||
supported public workaround is to write and terminate separately:
|
||||
|
||||
```python
|
||||
typer.echo("Error: broken", err=True)
|
||||
raise typer.Exit(code=1)
|
||||
```
|
||||
|
||||
This works, but each application must reproduce the error prefix, output
|
||||
stream, formatting policy, and exit behavior instead of expressing one typed
|
||||
CLI error.
|
||||
|
||||
`typer.Abort` is not an equivalent because it represents user cancellation and
|
||||
adds `Aborted!` output.
|
||||
|
||||
### Proposed API
|
||||
|
||||
Expose the vendored general-purpose exception through Typer's public namespace,
|
||||
or provide a Typer-native equivalent:
|
||||
|
||||
```python
|
||||
import typer
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
|
||||
@app.command()
|
||||
def fail() -> None:
|
||||
raise typer.ClickException("broken")
|
||||
```
|
||||
|
||||
Expected output should use Typer's standard concise error formatting and be
|
||||
captured by `typer.testing.CliRunner` on stderr.
|
||||
|
||||
The exact public name is not important. A Typer-native `UsageError` or another
|
||||
documented general-purpose CLI error would satisfy the same need.
|
||||
|
||||
### Why This Is Distinct From Existing Exports
|
||||
|
||||
- `typer.BadParameter` describes parameter validation and requires parameter
|
||||
context for its best output.
|
||||
- `typer.Abort` describes cancellation and prints `Aborted!`.
|
||||
- `typer.Exit` controls termination but carries no error message or formatting.
|
||||
|
||||
A general-purpose error is useful when adapting failures from HTTP clients,
|
||||
RPC calls, configuration loading, or other application services at the CLI
|
||||
boundary.
|
||||
|
||||
### Version Context
|
||||
|
||||
- Typer 0.25.0 with external Click: `click.ClickException` produced concise
|
||||
output.
|
||||
- Typer 0.26.0 and newer: direct Click use is intentionally unsupported.
|
||||
- Typer 0.26.8 and `master` at `b210c0e2`: no public general-purpose Typer
|
||||
exception is exported.
|
||||
|
||||
Test environment: Python 3.14.3 on Windows 11.
|
||||
+170
-188
@@ -1,240 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from wf_mcp.models import BrokerConfig
|
||||
from wf_mcp.server import create_server_client
|
||||
|
||||
from ..test_support import local_temp_root
|
||||
from .conftest import assert_safe_tool_maps, server_config
|
||||
|
||||
|
||||
def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
|
||||
config = server_config()
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(config, search_tools=True)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
names = [tool.name for tool in tools]
|
||||
client = create_server_client(config, search_tools=True)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
names = [tool.name for tool in tools]
|
||||
|
||||
assert "search_tools" in names
|
||||
assert "call_tool" in names
|
||||
assert "wf.admin.list_sources" in names
|
||||
assert "wf.admin.list_connections" in names
|
||||
assert "wf.admin.get_connection_statuses" in names
|
||||
assert "wf.admin.reload_config" in names
|
||||
assert "wf.admin.list_proxy_tools" in names
|
||||
assert "wf.admin.get_proxy_tool" in names
|
||||
assert "wf.workflow.list_artifacts" in names
|
||||
assert "wf.workflow.list_capabilities" in names
|
||||
assert "wf.workflow.inspect_capability" in names
|
||||
assert "wf.workflow.call_capability" in names
|
||||
assert "wf.workflow.list_draft_workspaces" in names
|
||||
assert "wf.workflow.create_draft_workspace" in names
|
||||
assert "wf.workflow.get_draft_workspace" in names
|
||||
assert "wf.workflow.delete_draft_workspace" in names
|
||||
assert "wf.workflow.patch_draft_workspace" in names
|
||||
assert "wf.workflow.validate_draft_workspace" in names
|
||||
assert "wf.workflow.set_draft_name" in names
|
||||
assert "wf.workflow.set_draft_route" in names
|
||||
assert "wf.workflow.set_step_input_map" in names
|
||||
assert "wf.workflow.set_step_output_map" in names
|
||||
assert "wf.workflow.set_workflow_output_map" in names
|
||||
assert "wf.workflow.bind" in names
|
||||
assert "wf.workflow.remove_draft_route" in names
|
||||
assert "wf.workflow.remove_draft_step" in names
|
||||
assert "wf.workflow.remove_draft_binding" in names
|
||||
assert "wf.workflow.create_minimal_draft_workspace" in names
|
||||
assert "wf.workflow.create_artifact_from_workspace" in names
|
||||
assert "wf.workflow.create_wrapper_from_workspace" in names
|
||||
assert "wf.workflow.inspect_artifact" in names
|
||||
assert "wf.workflow.list_deployments" in names
|
||||
assert "wf.workflow.inspect_deployment" in names
|
||||
assert "wf.workflow.save_deployment" in names
|
||||
assert "wf.workflow.delete_deployment" in names
|
||||
assert "wf.workflow.validate_deployment" in names
|
||||
assert "wf.workflow.run_deployment" in names
|
||||
assert "search_tools" in names
|
||||
assert "call_tool" in names
|
||||
assert "wf.admin.list_sources" in names
|
||||
assert "wf.admin.list_connections" in names
|
||||
assert "wf.admin.get_connection_statuses" in names
|
||||
assert "wf.admin.reload_config" in names
|
||||
assert "wf.admin.list_proxy_tools" in names
|
||||
assert "wf.admin.get_proxy_tool" in names
|
||||
assert "wf.workflow.list_artifacts" in names
|
||||
assert "wf.workflow.list_capabilities" in names
|
||||
assert "wf.workflow.inspect_capability" in names
|
||||
assert "wf.workflow.call_capability" in names
|
||||
assert "wf.workflow.list_draft_workspaces" in names
|
||||
assert "wf.workflow.create_draft_workspace" in names
|
||||
assert "wf.workflow.get_draft_workspace" in names
|
||||
assert "wf.workflow.delete_draft_workspace" in names
|
||||
assert "wf.workflow.patch_draft_workspace" in names
|
||||
assert "wf.workflow.validate_draft_workspace" in names
|
||||
assert "wf.workflow.set_draft_name" in names
|
||||
assert "wf.workflow.set_draft_route" in names
|
||||
assert "wf.workflow.set_step_input_map" in names
|
||||
assert "wf.workflow.set_step_output_map" in names
|
||||
assert "wf.workflow.set_workflow_output_map" in names
|
||||
assert "wf.workflow.bind" in names
|
||||
assert "wf.workflow.remove_draft_route" in names
|
||||
assert "wf.workflow.remove_draft_step" in names
|
||||
assert "wf.workflow.remove_draft_binding" in names
|
||||
assert "wf.workflow.create_minimal_draft_workspace" in names
|
||||
assert "wf.workflow.create_artifact_from_workspace" in names
|
||||
assert "wf.workflow.create_wrapper_from_workspace" in names
|
||||
assert "wf.workflow.inspect_artifact" in names
|
||||
assert "wf.workflow.list_deployments" in names
|
||||
assert "wf.workflow.inspect_deployment" in names
|
||||
assert "wf.workflow.save_deployment" in names
|
||||
assert "wf.workflow.delete_deployment" in names
|
||||
assert "wf.workflow.validate_deployment" in names
|
||||
assert "wf.workflow.run_deployment" in names
|
||||
|
||||
assert "wf.workflow.validate_draft" not in names
|
||||
assert "wf.workflow.compile_draft" not in names
|
||||
assert "wf.workflow.create_artifact_from_plan" not in names
|
||||
assert "wf.workflow.create_artifact_from_draft" not in names
|
||||
assert "wf.workflow.patch_draft" not in names
|
||||
assert "wf.admin.call_tool" not in names
|
||||
assert "fixture.personal.echo_tool" not in names
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
assert "wf.workflow.validate_draft" not in names
|
||||
assert "wf.workflow.compile_draft" not in names
|
||||
assert "wf.workflow.create_artifact_from_plan" not in names
|
||||
assert "wf.workflow.create_artifact_from_draft" not in names
|
||||
assert "wf.workflow.patch_draft" not in names
|
||||
assert "wf.admin.call_tool" not in names
|
||||
assert "fixture.personal.echo_tool" not in names
|
||||
|
||||
|
||||
def test_server_search_mode_can_use_safe_tool_names() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_search_mode_can_use_safe_tool_names() -> None:
|
||||
config = server_config()
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(
|
||||
config,
|
||||
search_tools=True,
|
||||
safe_tool_names=True,
|
||||
client = create_server_client(
|
||||
config,
|
||||
search_tools=True,
|
||||
safe_tool_names=True,
|
||||
)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
names = [tool.name for tool in tools]
|
||||
|
||||
assert "search_tools" in names
|
||||
assert "call_tool" in names
|
||||
assert "wf_admin_list_sources" in names
|
||||
assert "wf_workflow_call_capability" in names
|
||||
assert "wf.admin.list_sources" not in names
|
||||
|
||||
result = await assert_safe_tool_maps(
|
||||
client,
|
||||
original_name="wf.admin.list_sources",
|
||||
safe_name="wf_admin_list_sources",
|
||||
)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
names = [tool.name for tool in tools]
|
||||
|
||||
assert "search_tools" in names
|
||||
assert "call_tool" in names
|
||||
assert "wf_admin_list_sources" in names
|
||||
assert "wf_workflow_call_capability" in names
|
||||
assert "wf.admin.list_sources" not in names
|
||||
|
||||
result = await assert_safe_tool_maps(
|
||||
client,
|
||||
original_name="wf.admin.list_sources",
|
||||
safe_name="wf_admin_list_sources",
|
||||
)
|
||||
source_ids = {source["id"] for source in result["sources"]}
|
||||
assert "wf.std" in source_ids
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
source_ids = {source["id"] for source in result["sources"]}
|
||||
assert "wf.std" in source_ids
|
||||
|
||||
|
||||
def test_server_safe_tool_names_adapts_dotted_runtime_names() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_server_safe_tool_names_adapts_dotted_runtime_names() -> None:
|
||||
config = server_config()
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(config, safe_tool_names=True)
|
||||
async with client:
|
||||
artifacts = await assert_safe_tool_maps(
|
||||
client,
|
||||
original_name="wf.workflow.list_artifacts",
|
||||
safe_name="wf_workflow_list_artifacts",
|
||||
)
|
||||
echo = await assert_safe_tool_maps(
|
||||
client,
|
||||
original_name="fixture.personal.echo_tool",
|
||||
safe_name="fixture_personal_echo_tool",
|
||||
arguments={"text": "hello"},
|
||||
)
|
||||
client = create_server_client(config, safe_tool_names=True)
|
||||
async with client:
|
||||
artifacts = await assert_safe_tool_maps(
|
||||
client,
|
||||
original_name="wf.workflow.list_artifacts",
|
||||
safe_name="wf_workflow_list_artifacts",
|
||||
)
|
||||
echo = await assert_safe_tool_maps(
|
||||
client,
|
||||
original_name="fixture.personal.echo_tool",
|
||||
safe_name="fixture_personal_echo_tool",
|
||||
arguments={"text": "hello"},
|
||||
)
|
||||
|
||||
assert artifacts["nodes"] == []
|
||||
assert artifacts["total"] == 0
|
||||
assert echo["echoed"] == "hello"
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
assert artifacts["nodes"] == []
|
||||
assert artifacts["total"] == 0
|
||||
assert echo["echoed"] == "hello"
|
||||
|
||||
|
||||
def test_workflow_tools_have_human_metadata() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_tools_have_human_metadata(tmp_path: Path) -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "unified_metadata_store",
|
||||
store_root=tmp_path / "unified_metadata_store",
|
||||
connections=[],
|
||||
)
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(config, admin_tools=False)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
list_artifacts = by_name["wf.workflow.list_artifacts"]
|
||||
validate_deployment = by_name["wf.workflow.validate_deployment"]
|
||||
run_deployment = by_name["wf.workflow.run_deployment"]
|
||||
inspect_run = by_name["wf.workflow.inspect_run"]
|
||||
read_run_trace = by_name["wf.workflow.read_run_trace"]
|
||||
client = create_server_client(config, admin_tools=False)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
list_artifacts = by_name["wf.workflow.list_artifacts"]
|
||||
validate_deployment = by_name["wf.workflow.validate_deployment"]
|
||||
run_deployment = by_name["wf.workflow.run_deployment"]
|
||||
inspect_run = by_name["wf.workflow.inspect_run"]
|
||||
read_run_trace = by_name["wf.workflow.read_run_trace"]
|
||||
|
||||
assert list_artifacts.title == "List Workflow Artifacts"
|
||||
assert "saved workflow artifacts" in (list_artifacts.description or "")
|
||||
assert "query" in list_artifacts.inputSchema["properties"]
|
||||
assert "kind" in list_artifacts.inputSchema["properties"]
|
||||
assert "cursor" in list_artifacts.inputSchema["properties"]
|
||||
assert "limit" in list_artifacts.inputSchema["properties"]
|
||||
live_check_schema = validate_deployment.inputSchema["properties"][
|
||||
"live_check"
|
||||
]
|
||||
assert "upstream" in live_check_schema.get("description", "")
|
||||
assert run_deployment.title == "Run Workflow Deployment"
|
||||
assert "deployment_id" in (run_deployment.description or "")
|
||||
assert "trace_range" in run_deployment.inputSchema["properties"]
|
||||
trace_range_schema = run_deployment.inputSchema["properties"]["trace_range"]
|
||||
assert "Debug traces" in trace_range_schema.get("description", "")
|
||||
assert "null" in [
|
||||
option.get("type") for option in trace_range_schema["anyOf"]
|
||||
]
|
||||
assert inspect_run.title == "Inspect Workflow Run"
|
||||
assert "trace" in (inspect_run.description or "").lower()
|
||||
read_trace_schema = read_run_trace.inputSchema["properties"]["trace_range"]
|
||||
assert "Debug traces" in read_trace_schema.get("description", "")
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
assert list_artifacts.title == "List Workflow Artifacts"
|
||||
assert "saved workflow artifacts" in (list_artifacts.description or "")
|
||||
assert "query" in list_artifacts.inputSchema["properties"]
|
||||
assert "kind" in list_artifacts.inputSchema["properties"]
|
||||
assert "cursor" in list_artifacts.inputSchema["properties"]
|
||||
assert "limit" in list_artifacts.inputSchema["properties"]
|
||||
live_check_schema = validate_deployment.inputSchema["properties"]["live_check"]
|
||||
assert "upstream" in live_check_schema.get("description", "")
|
||||
assert run_deployment.title == "Run Workflow Deployment"
|
||||
assert "deployment_id" in (run_deployment.description or "")
|
||||
assert "trace_range" in run_deployment.inputSchema["properties"]
|
||||
trace_range_schema = run_deployment.inputSchema["properties"]["trace_range"]
|
||||
assert "Debug traces" in trace_range_schema.get("description", "")
|
||||
assert "null" in [option.get("type") for option in trace_range_schema["anyOf"]]
|
||||
assert inspect_run.title == "Inspect Workflow Run"
|
||||
assert "trace" in (inspect_run.description or "").lower()
|
||||
read_trace_schema = read_run_trace.inputSchema["properties"]["trace_range"]
|
||||
assert "Debug traces" in read_trace_schema.get("description", "")
|
||||
|
||||
|
||||
def test_create_artifact_from_plan_exposes_plan_as_plain_object() -> None:
|
||||
async def test_create_artifact_from_plan_exposes_plan_as_plain_object(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "unified_create_artifact_schema_store",
|
||||
store_root=tmp_path / "unified_create_artifact_schema_store",
|
||||
connections=[],
|
||||
)
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(config, admin_tools=False)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
schema = by_name["wf.workflow.create_artifact_from_plan"].inputSchema
|
||||
plan_schema = schema["properties"]["plan"]
|
||||
client = create_server_client(config, admin_tools=False)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
schema = by_name["wf.workflow.create_artifact_from_plan"].inputSchema
|
||||
plan_schema = schema["properties"]["plan"]
|
||||
|
||||
assert plan_schema["type"] == "object"
|
||||
assert plan_schema.get("additionalProperties") is True
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
assert plan_schema["type"] == "object"
|
||||
assert plan_schema.get("additionalProperties") is True
|
||||
|
||||
|
||||
def test_draft_tools_expose_plain_object_and_patch_array_schemas() -> None:
|
||||
async def test_draft_tools_expose_plain_object_and_patch_array_schemas(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "unified_draft_schema_store",
|
||||
store_root=tmp_path / "unified_draft_schema_store",
|
||||
connections=[],
|
||||
)
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(config, admin_tools=False)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
client = create_server_client(config, admin_tools=False)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
|
||||
validate_schema = by_name["wf.workflow.validate_draft"].inputSchema
|
||||
validate_draft_schema = validate_schema["properties"]["draft"]
|
||||
create_schema = by_name[
|
||||
"wf.workflow.create_artifact_from_draft"
|
||||
].inputSchema
|
||||
create_draft_schema = create_schema["properties"]["draft"]
|
||||
patch_schema = by_name["wf.workflow.patch_draft"].inputSchema
|
||||
patch_draft_schema = patch_schema["properties"]["draft"]
|
||||
patch_patch_schema = patch_schema["properties"]["patch"]
|
||||
validate_schema = by_name["wf.workflow.validate_draft"].inputSchema
|
||||
validate_draft_schema = validate_schema["properties"]["draft"]
|
||||
create_schema = by_name["wf.workflow.create_artifact_from_draft"].inputSchema
|
||||
create_draft_schema = create_schema["properties"]["draft"]
|
||||
patch_schema = by_name["wf.workflow.patch_draft"].inputSchema
|
||||
patch_draft_schema = patch_schema["properties"]["draft"]
|
||||
patch_patch_schema = patch_schema["properties"]["patch"]
|
||||
|
||||
assert validate_draft_schema["type"] == "object"
|
||||
assert validate_draft_schema.get("additionalProperties") is True
|
||||
assert create_draft_schema["type"] == "object"
|
||||
assert create_draft_schema.get("additionalProperties") is True
|
||||
assert patch_draft_schema["type"] == "object"
|
||||
assert patch_patch_schema["type"] == "array"
|
||||
assert "$defs" not in patch_patch_schema
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
assert validate_draft_schema["type"] == "object"
|
||||
assert validate_draft_schema.get("additionalProperties") is True
|
||||
assert create_draft_schema["type"] == "object"
|
||||
assert create_draft_schema.get("additionalProperties") is True
|
||||
assert patch_draft_schema["type"] == "object"
|
||||
assert patch_patch_schema["type"] == "array"
|
||||
assert "$defs" not in patch_patch_schema
|
||||
|
||||
|
||||
def test_admin_tools_have_human_metadata() -> None:
|
||||
async def test_admin_tools_have_human_metadata(tmp_path: Path) -> None:
|
||||
config = BrokerConfig(
|
||||
store_root=local_temp_root() / "unified_admin_metadata_store",
|
||||
store_root=tmp_path / "unified_admin_metadata_store",
|
||||
connections=[],
|
||||
)
|
||||
|
||||
async def run_proxy() -> None:
|
||||
client = create_server_client(config)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
list_connections = by_name["wf.admin.list_connections"]
|
||||
reload_config = by_name["wf.admin.reload_config"]
|
||||
client = create_server_client(config)
|
||||
async with client:
|
||||
tools = await client.list_tools()
|
||||
by_name = {tool.name: tool for tool in tools}
|
||||
list_connections = by_name["wf.admin.list_connections"]
|
||||
reload_config = by_name["wf.admin.reload_config"]
|
||||
|
||||
assert list_connections.title == "List Connections"
|
||||
assert "configured MCP connections" in (list_connections.description or "")
|
||||
assert reload_config.title == "Reload Config"
|
||||
assert "remount" in (reload_config.description or "")
|
||||
|
||||
asyncio.run(run_proxy())
|
||||
assert list_connections.title == "List Connections"
|
||||
assert "configured MCP connections" in (list_connections.description or "")
|
||||
assert reload_config.title == "Reload Config"
|
||||
assert "remount" in (reload_config.description or "")
|
||||
|
||||
Reference in New Issue
Block a user