feat: route cap and run cli commands to rpc targets

This commit is contained in:
lda
2026-06-03 08:38:58 +07:00 Verified
parent b9b60250e8
commit 673c99a70f
5 changed files with 114 additions and 16 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ from typing import Annotated
import typer import typer
from wf_cli.context import config_path_from_context, load_cli_context from wf_cli.context import load_cli_context_from_typer
from wf_cli.formats import ListOutputFormat, emit_list_payload from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import emit_json from wf_cli.io import emit_json
@@ -37,7 +37,7 @@ def list_capabilities(
] = ListOutputFormat.JSON, ] = ListOutputFormat.JSON,
) -> None: ) -> None:
"""List compact planner-visible workflow capabilities.""" """List compact planner-visible workflow capabilities."""
context = load_cli_context(config_path_from_context(ctx)) context = load_cli_context_from_typer(ctx)
payload = asyncio.run( payload = asyncio.run(
context.handlers.list_capabilities( context.handlers.list_capabilities(
query=query, query=query,
@@ -61,7 +61,7 @@ def inspect_capability(
qualified_name: Annotated[str, typer.Argument(help="Workflow capability name.")], qualified_name: Annotated[str, typer.Argument(help="Workflow capability name.")],
) -> None: ) -> None:
"""Inspect one workflow capability contract.""" """Inspect one workflow capability contract."""
context = load_cli_context(config_path_from_context(ctx)) context = load_cli_context_from_typer(ctx)
payload = asyncio.run( payload = asyncio.run(
context.handlers.inspect_capability(qualified_name=qualified_name) context.handlers.inspect_capability(qualified_name=qualified_name)
) )
+4 -4
View File
@@ -6,7 +6,7 @@ from typing import Annotated
import typer import typer
from wf_cli.context import config_path_from_context, load_cli_context from wf_cli.context import load_cli_context_from_typer
from wf_cli.io import CliInputError, emit_json, parse_json_input from wf_cli.io import CliInputError, emit_json, parse_json_input
from wf_api import TraceRange from wf_api import TraceRange
@@ -45,7 +45,7 @@ def start_run(
workflow_input = parse_json_input(input_json=input_json, input_file=input_file) workflow_input = parse_json_input(input_json=input_json, input_file=input_file)
except CliInputError as exc: except CliInputError as exc:
raise typer.BadParameter(str(exc)) from exc raise typer.BadParameter(str(exc)) from exc
context = load_cli_context(config_path_from_context(ctx)) context = load_cli_context_from_typer(ctx)
trace_range = _optional_trace_range(start=trace_from, limit=trace_limit) trace_range = _optional_trace_range(start=trace_from, limit=trace_limit)
payload = asyncio.run( payload = asyncio.run(
context.handlers.run_deployment( context.handlers.run_deployment(
@@ -63,7 +63,7 @@ def inspect_run(
run_id: Annotated[str, typer.Argument(help="Durable run id to inspect.")], run_id: Annotated[str, typer.Argument(help="Durable run id to inspect.")],
) -> None: ) -> None:
"""Inspect a durable run without trace entries.""" """Inspect a durable run without trace entries."""
context = load_cli_context(config_path_from_context(ctx)) context = load_cli_context_from_typer(ctx)
emit_json(asyncio.run(context.handlers.inspect_run(run_id=run_id))) emit_json(asyncio.run(context.handlers.inspect_run(run_id=run_id)))
@@ -81,7 +81,7 @@ def trace_run(
] = 25, ] = 25,
) -> None: ) -> None:
"""Read a bounded debug trace slice.""" """Read a bounded debug trace slice."""
context = load_cli_context(config_path_from_context(ctx)) context = load_cli_context_from_typer(ctx)
payload = asyncio.run( payload = asyncio.run(
context.handlers.read_run_trace( context.handlers.read_run_trace(
run_id=run_id, run_id=run_id,
+10 -4
View File
@@ -132,7 +132,13 @@ def _write_cli_config(root: Path) -> Path:
return config_path return config_path
def _load_cli_context_with_specs(config_path: str | Path) -> CliContext: from wf_cli.context import load_cli_context_from_typer, config_path_from_context, load_cli_context
def _load_cli_context_with_specs(ctx: typer.Context | str | Path) -> CliContext:
if isinstance(ctx, (str, Path)):
config_path = ctx
else:
config_path = config_path_from_context(ctx)
"""Seed executable demo specs for CLI tests only. """Seed executable demo specs for CLI tests only.
Config loading registers connections and stores; it does not register Config loading registers connections and stores; it does not register
@@ -149,7 +155,7 @@ def test_wf_cap_list_outputs_json() -> None:
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _write_cli_config(root) config_path = _write_cli_config(root)
with patch("wf_cli.commands.caps.load_cli_context", _load_cli_context_with_specs): with patch("wf_cli.commands.caps.load_cli_context_from_typer", _load_cli_context_with_specs):
result = runner.invoke( result = runner.invoke(
app, app,
["--config", str(config_path), "cap", "list", "--source", "demo.personal"], ["--config", str(config_path), "cap", "list", "--source", "demo.personal"],
@@ -166,7 +172,7 @@ def test_wf_cap_list_ids_format() -> None:
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _write_cli_config(root) config_path = _write_cli_config(root)
with patch("wf_cli.commands.caps.load_cli_context", _load_cli_context_with_specs): with patch("wf_cli.commands.caps.load_cli_context_from_typer", _load_cli_context_with_specs):
result = runner.invoke( result = runner.invoke(
app, app,
[ [
@@ -190,7 +196,7 @@ def test_wf_cap_inspect_outputs_detail() -> None:
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _write_cli_config(root) config_path = _write_cli_config(root)
with patch("wf_cli.commands.caps.load_cli_context", _load_cli_context_with_specs): with patch("wf_cli.commands.caps.load_cli_context_from_typer", _load_cli_context_with_specs):
result = runner.invoke( result = runner.invoke(
app, app,
["--config", str(config_path), "cap", "inspect", "demo.personal.echo_tool"], ["--config", str(config_path), "cap", "inspect", "demo.personal.echo_tool"],
+86
View File
@@ -57,3 +57,89 @@ def test_load_cli_context_local_override_beats_rpc_config(tmp_path) -> None:
assert not isinstance(context.handlers, RpcWorkflowApiClient) assert not isinstance(context.handlers, RpcWorkflowApiClient)
assert context.service is None assert context.service is None
assert context.config_path == config_path assert context.config_path == config_path
import asyncio
import httpx
from typer.testing import CliRunner
from wf_api.models import RawWorkflowPlan
from wf_cli.app import app
from wf_core import END
from wf_server import build_local_static_workflow_server
from wf_transport_rpc_http import create_rpc_app
def _constant_plan() -> RawWorkflowPlan:
return RawWorkflowPlan.model_validate(
{
"name": "remote_cli_constant",
"input_schema": {"type": "object", "properties": {}},
"state_schema": {
"type": "object",
"properties": {
"result": {"type": "string", "reducer": "wf.std.replace"}
},
},
"output_schema": {
"type": "object",
"properties": {"result": {"type": "string"}},
"required": ["result"],
},
"outcomes": ["ok"],
"start": "constant",
"nodes": [
{
"id": "constant",
"type": "node",
"node": "wf.std.constant",
"input": [
{
"value": "hello remote cli",
"target": {"root": "local", "parts": ["value"]},
}
],
"output": [
{
"source": {"root": "local", "parts": ["value"]},
"target": {"root": "state", "parts": ["result"]},
}
],
}
],
"edges": [{"from": "constant", "outcome": "ok", "to": END}],
"output": [
{
"path": {"root": "state", "parts": ["result"]},
"target": {"root": "local", "parts": ["result"]},
}
],
}
)
def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
rpc_app = create_rpc_app(server)
transport = httpx.ASGITransport(app=rpc_app)
original_client = httpx.AsyncClient
monkeypatch.setattr(
"wf_transport_rpc_http.client.httpx.AsyncClient",
lambda *args, **kwargs: original_client(transport=transport, base_url="http://test"),
)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
result = CliRunner().invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"cap",
"inspect",
"wf.std.constant",
],
)
assert result.exit_code == 0, result.output
assert '"name": "wf.std.constant"' in result.output
+11 -5
View File
@@ -53,7 +53,13 @@ def _seed_echo_deployment(root: Path) -> Path:
return config_path return config_path
def _load_cli_context_with_specs(config_path: str | Path) -> CliContext: from wf_cli.context import load_cli_context_from_typer, config_path_from_context, load_cli_context
def _load_cli_context_with_specs(ctx: typer.Context | str | Path) -> CliContext:
if isinstance(ctx, (str, Path)):
config_path = ctx
else:
config_path = config_path_from_context(ctx)
"""Test-only hook: seed executable demo specs for CLI integration tests. """Test-only hook: seed executable demo specs for CLI integration tests.
``build_service_from_config`` registers connections, adapters, and ``build_service_from_config`` registers connections, adapters, and
@@ -93,7 +99,7 @@ def test_wf_run_start_accepts_inline_json_input() -> None:
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
with patch("wf_cli.commands.runs.load_cli_context", _load_cli_context_with_specs): with patch("wf_cli.commands.runs.load_cli_context_from_typer", _load_cli_context_with_specs):
result = runner.invoke( result = runner.invoke(
app, app,
[ [
@@ -122,7 +128,7 @@ def test_wf_run_start_accepts_input_file() -> None:
input_path = root / "input.json" input_path = root / "input.json"
input_path.write_text('{"text": "from file"}', encoding="utf-8") input_path.write_text('{"text": "from file"}', encoding="utf-8")
with patch("wf_cli.commands.runs.load_cli_context", _load_cli_context_with_specs): with patch("wf_cli.commands.runs.load_cli_context_from_typer", _load_cli_context_with_specs):
result = runner.invoke( result = runner.invoke(
app, app,
[ [
@@ -147,7 +153,7 @@ def test_wf_run_inspect_and_trace_existing_run() -> None:
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
with patch("wf_cli.commands.runs.load_cli_context", _load_cli_context_with_specs): with patch("wf_cli.commands.runs.load_cli_context_from_typer", _load_cli_context_with_specs):
start = runner.invoke( start = runner.invoke(
app, app,
[ [
@@ -200,7 +206,7 @@ def test_wf_run_start_reports_bad_json() -> None:
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
config_path = _seed_echo_deployment(root) config_path = _seed_echo_deployment(root)
with patch("wf_cli.commands.runs.load_cli_context", _load_cli_context_with_specs): with patch("wf_cli.commands.runs.load_cli_context_from_typer", _load_cli_context_with_specs):
result = runner.invoke( result = runner.invoke(
app, app,
[ [