feat: route cap and run cli commands to rpc targets
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Annotated
|
||||
|
||||
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.io import emit_json
|
||||
|
||||
@@ -37,7 +37,7 @@ def list_capabilities(
|
||||
] = ListOutputFormat.JSON,
|
||||
) -> None:
|
||||
"""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(
|
||||
context.handlers.list_capabilities(
|
||||
query=query,
|
||||
@@ -61,7 +61,7 @@ def inspect_capability(
|
||||
qualified_name: Annotated[str, typer.Argument(help="Workflow capability name.")],
|
||||
) -> None:
|
||||
"""Inspect one workflow capability contract."""
|
||||
context = load_cli_context(config_path_from_context(ctx))
|
||||
context = load_cli_context_from_typer(ctx)
|
||||
payload = asyncio.run(
|
||||
context.handlers.inspect_capability(qualified_name=qualified_name)
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Annotated
|
||||
|
||||
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_api import TraceRange
|
||||
|
||||
@@ -45,7 +45,7 @@ def start_run(
|
||||
workflow_input = parse_json_input(input_json=input_json, input_file=input_file)
|
||||
except CliInputError as 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)
|
||||
payload = asyncio.run(
|
||||
context.handlers.run_deployment(
|
||||
@@ -63,7 +63,7 @@ def inspect_run(
|
||||
run_id: Annotated[str, typer.Argument(help="Durable run id to inspect.")],
|
||||
) -> None:
|
||||
"""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)))
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ def trace_run(
|
||||
] = 25,
|
||||
) -> None:
|
||||
"""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(
|
||||
context.handlers.read_run_trace(
|
||||
run_id=run_id,
|
||||
|
||||
@@ -132,7 +132,13 @@ def _write_cli_config(root: Path) -> 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.
|
||||
|
||||
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)
|
||||
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(
|
||||
app,
|
||||
["--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)
|
||||
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(
|
||||
app,
|
||||
[
|
||||
@@ -190,7 +196,7 @@ def test_wf_cap_inspect_outputs_detail() -> None:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
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(
|
||||
app,
|
||||
["--config", str(config_path), "cap", "inspect", "demo.personal.echo_tool"],
|
||||
|
||||
@@ -57,3 +57,89 @@ def test_load_cli_context_local_override_beats_rpc_config(tmp_path) -> None:
|
||||
assert not isinstance(context.handlers, RpcWorkflowApiClient)
|
||||
assert context.service is None
|
||||
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
|
||||
|
||||
@@ -53,7 +53,13 @@ def _seed_echo_deployment(root: Path) -> 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.
|
||||
|
||||
``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)
|
||||
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(
|
||||
app,
|
||||
[
|
||||
@@ -122,7 +128,7 @@ def test_wf_run_start_accepts_input_file() -> None:
|
||||
input_path = root / "input.json"
|
||||
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(
|
||||
app,
|
||||
[
|
||||
@@ -147,7 +153,7 @@ def test_wf_run_inspect_and_trace_existing_run() -> None:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
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(
|
||||
app,
|
||||
[
|
||||
@@ -200,7 +206,7 @@ def test_wf_run_start_reports_bad_json() -> None:
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
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(
|
||||
app,
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user