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
|
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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
[
|
[
|
||||||
|
|||||||
Reference in New Issue
Block a user