Files
lda-wf/tests/wf_cli/test_remote_target.py
T

1602 lines
47 KiB
Python

from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Any, cast
import httpx
from typer.testing import CliRunner
import wf_cli.context as cli_context
from wf_api.models import RawWorkflowPlan
from wf_cli.app import app
from wf_cli.context import CliContext, load_cli_context, load_local_cli_context
from wf_core import END
from wf_server import build_local_static_workflow_server
from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app
from wf_transport_rpc_http.client.sources import RpcSourceAdminClientMixin
from .conftest import write_python_source_config
class BrokenSourceAdmin:
async def list_sources(
self,
*,
cursor: str | None = None,
limit: int = 50,
) -> dict[str, Any]:
return {"sources": [], "next_cursor": None, "total": 0}
async def inspect_source(self, *, source_id: str) -> dict[str, Any]:
raise RuntimeError(f"broken source admin for {source_id}")
async def diagnose_source(self, *, source_id: str) -> dict[str, Any]:
raise RuntimeError(f"broken source admin for {source_id}")
class InventorySourceAdmin:
async def list_sources(
self,
*,
cursor: str | None = None,
limit: int = 50,
) -> dict[str, Any]:
return {"sources": [], "next_cursor": None, "total": 0}
async def inspect_source(self, *, source_id: str) -> dict[str, Any]:
return {
"id": source_id,
"capabilities": {
"resources": [
f"{source_id}.architecture.md",
f"{source_id}.startup.md",
],
"prompts": [
f"{source_id}.simple-prompt",
f"{source_id}.args-prompt",
],
},
}
async def diagnose_source(self, *, source_id: str) -> dict[str, Any]:
return {"source_id": source_id, "status": "ok"}
def test_load_cli_context_uses_rpc_client_for_rpc_http_target(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
"timeout_seconds": 9,
}
},
}
),
encoding="utf-8",
)
context = load_cli_context(config_path)
assert isinstance(context.handlers, RpcWorkflowApiClient)
assert context.handlers.url == "http://127.0.0.1:8765/rpc"
assert context.handlers.timeout_seconds == 9
assert context.service is None
def test_load_cli_context_local_override_beats_rpc_config(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
}
},
"server": {
"store": {"kind": "filesystem", "root": ".wf_store"},
},
}
),
encoding="utf-8",
)
context = load_cli_context(config_path, force_local=True)
assert not isinstance(context.handlers, RpcWorkflowApiClient)
assert context.service is None
assert context.config_path == config_path
def test_load_cli_context_rejects_local_and_url_conflict(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
try:
load_cli_context(
config_path,
force_local=True,
rpc_url="http://127.0.0.1:8765/rpc",
)
except ValueError as exc:
message = str(exc)
else:
raise AssertionError("expected ValueError")
assert "--local and --url are mutually exclusive" in message
def test_load_cli_context_uses_workflow_shape_not_filename(tmp_path) -> None:
config_path = tmp_path / "wf_mcp.config.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
}
},
}
),
encoding="utf-8",
)
context = load_cli_context(config_path)
assert isinstance(context.handlers, RpcWorkflowApiClient)
def test_load_cli_context_uses_broker_shape_not_filename(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps({"store_root": ".wf_mcp_store", "connections": []}),
encoding="utf-8",
)
context = load_cli_context(config_path)
assert context.service is not None
assert not isinstance(context.handlers, RpcWorkflowApiClient)
def test_load_cli_context_url_override_reuses_config_timeout(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
"timeout_seconds": 77,
}
},
}
),
encoding="utf-8",
)
context = load_cli_context(config_path, rpc_url="http://127.0.0.1:9999/rpc")
assert isinstance(context.handlers, RpcWorkflowApiClient)
assert context.handlers.url == "http://127.0.0.1:9999/rpc"
assert context.handlers.timeout_seconds == 77
def test_local_cli_context_rejects_rpc_target_for_local_only_commands(tmp_path) -> None:
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://127.0.0.1:8765/rpc",
}
},
}
),
encoding="utf-8",
)
try:
load_local_cli_context(config_path)
except ValueError as exc:
message = str(exc)
else:
raise AssertionError("expected ValueError")
assert "not available for rpc_http targets yet" in message
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 _interrupt_plan() -> RawWorkflowPlan:
return RawWorkflowPlan.model_validate(
{
"name": "remote_approval",
"input_schema": {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
},
"state_schema": {"fields": {}},
"output_schema": {"type": "object", "properties": {}},
"outcomes": ["submitted"],
"start": "approval",
"nodes": [
{
"id": "approval",
"type": "interrupt",
"kind": "approval",
"request": [
{
"path": {"root": "input", "parts": ["message"]},
"target": {"root": "local", "parts": ["message"]},
}
],
"resume": [],
"outcomes": ["submitted"],
},
{"id": "end_submitted", "type": "end", "outcome": "submitted"},
],
"edges": [
{"from": "approval", "outcome": "submitted", "to": "end_submitted"}
],
}
)
def _patch_rpc_client_to_server(monkeypatch, server) -> None:
"""Route CLI-created RPC clients to an in-process ASGI test server."""
def fake_rpc_client_from_target(
*,
url: str,
timeout_seconds: float,
) -> RpcWorkflowApiClient:
return RpcWorkflowApiClient(
url=url,
timeout_seconds=timeout_seconds,
http_client=httpx.AsyncClient(
transport=httpx.ASGITransport(app=create_rpc_app(server)),
base_url="http://test",
),
)
monkeypatch.setattr(
cli_context, "rpc_client_from_target", fake_rpc_client_from_target
)
def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
inspected = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"cap",
"inspect",
"wf.std.constant",
],
)
listed = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"cap",
"list",
"--source",
"wf.std",
"--limit",
"100",
],
)
called = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"cap",
"call",
"wf.std.constant",
"--input",
'{"value": "hello cap call"}',
],
)
assert inspected.exit_code == 0, inspected.output
assert '"name": "wf.std.constant"' in inspected.output
assert listed.exit_code == 0, listed.output
listed_payload = json.loads(listed.output)
assert listed_payload["capabilities"]
assert {
capability["source_id"] for capability in listed_payload["capabilities"]
} == {"wf.std"}
assert called.exit_code == 0, called.output
called_payload = json.loads(called.output)
assert called_payload["qualified_name"] == "wf.std.constant"
assert called_payload["outcome"] == "ok"
assert called_payload["output"] == {"value": "hello cap call"}
compact = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"cap",
"call",
"wf.std.constant",
"--input",
'{"value": "hello cap call"}',
"--format",
"compact",
],
)
assert compact.exit_code == 0, compact.output
assert "wf.std.constant" in compact.output
assert "outcome=ok" in compact.output
assert "hello cap call" not in compact.output
help_result = runner.invoke(app, ["cap", "call", "--help"])
assert help_result.exit_code == 0
help_text = " ".join(help_result.output.split())
assert "--unwrap-text" in help_result.output
assert "MCP text content block" in help_text
assert "multiple blocks" in help_text
assert "non-MCP" in help_text
def test_wf_source_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
listed = runner.invoke(app, [*base_args, "source", "list", "--limit", "10"])
inspected = runner.invoke(app, [*base_args, "source", "inspect", "wf.std"])
assert listed.exit_code == 0, listed.output
assert '"id": "wf.std"' in listed.output
assert inspected.exit_code == 0, inspected.output
assert '"id": "wf.std"' in inspected.output
def test_wf_source_resources_and_prompts_render_names(monkeypatch, tmp_path) -> None:
fake_context = CliContext(
config_path=Path("dummy"),
service=cast(Any, object()),
handlers=build_local_static_workflow_server(tmp_path / "store").api,
source_admin=InventorySourceAdmin(),
admin=cast(Any, object()),
)
monkeypatch.setattr(
"wf_cli.commands.sources.load_cli_context_from_typer",
lambda _ctx: fake_context,
)
runner = CliRunner()
resources = runner.invoke(app, ["source", "resources", "everything.default"])
prompts = runner.invoke(app, ["source", "prompts", "everything.default"])
assert resources.exit_code == 0, resources.output
assert resources.output.splitlines() == [
"everything.default.architecture.md",
"everything.default.startup.md",
]
assert prompts.exit_code == 0, prompts.output
assert prompts.output.splitlines() == [
"everything.default.simple-prompt",
"everything.default.args-prompt",
]
def test_wf_source_resources_json_format(monkeypatch, tmp_path) -> None:
fake_context = CliContext(
config_path=Path("dummy"),
service=cast(Any, object()),
handlers=build_local_static_workflow_server(tmp_path / "store").api,
source_admin=InventorySourceAdmin(),
admin=cast(Any, object()),
)
monkeypatch.setattr(
"wf_cli.commands.sources.load_cli_context_from_typer",
lambda _ctx: fake_context,
)
result = CliRunner().invoke(
app,
["source", "resources", "everything.default", "--format", "json"],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload == {
"source_id": "everything.default",
"resources": [
"everything.default.architecture.md",
"everything.default.startup.md",
],
}
def test_wf_remote_source_inspect_formats_expected_rpc_error(
monkeypatch,
tmp_path,
) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
result = runner.invoke(app, [*base_args, "source", "inspect", "missing.source"])
assert result.exit_code != 0
assert "Error" in result.output
assert "Workflow operation failed" in result.output
assert "missing.source" in result.output
assert "Traceback" not in result.output
assert "RuntimeError" not in result.output
def test_wf_remote_source_list_formats_transport_error(monkeypatch, tmp_path) -> None:
async def connection_failed(*args: Any, **kwargs: Any) -> dict[str, Any]:
raise httpx.ConnectError(
"connection refused",
request=httpx.Request("POST", "http://test/rpc"),
)
monkeypatch.setattr(RpcSourceAdminClientMixin, "list_sources", connection_failed)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
result = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"source",
"list",
],
)
assert result.exit_code != 0
assert "Error" in result.output
assert "connection refused" in result.output
assert "Traceback" not in result.output
assert "ConnectError" not in result.output
def test_wf_unexpected_error_uses_short_traceback_by_default(
monkeypatch,
tmp_path,
) -> None:
fake_context = CliContext(
config_path=Path("dummy"),
service=cast(Any, object()),
handlers=build_local_static_workflow_server(tmp_path / "store").api,
source_admin=BrokenSourceAdmin(),
admin=cast(Any, object()),
)
monkeypatch.setattr(
"wf_cli.commands.sources.load_cli_context_from_typer",
lambda _ctx: fake_context,
)
result = CliRunner().invoke(app, ["source", "inspect", "wf.std"])
assert result.exit_code != 0
assert "broken source admin for wf.std" in result.output
assert "tests/wf_cli/test_remote_target.py" not in result.output
def test_wf_verbose_shows_full_traceback_for_unexpected_error(
monkeypatch,
tmp_path,
) -> None:
fake_context = CliContext(
config_path=Path("dummy"),
service=cast(Any, object()),
handlers=build_local_static_workflow_server(tmp_path / "store").api,
source_admin=BrokenSourceAdmin(),
admin=cast(Any, object()),
verbose=True,
)
monkeypatch.setattr(
"wf_cli.commands.sources.load_cli_context_from_typer",
lambda _ctx: fake_context,
)
result = CliRunner().invoke(app, ["--verbose", "source", "inspect", "wf.std"])
assert result.exit_code != 0
assert isinstance(result.exception, RuntimeError)
assert str(result.exception) == "broken source admin for wf.std"
def test_wf_admin_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
server.events.record_workflow_event(
"workflow_test_event",
capability_id="workflow.demo.v1",
payload={"ok": True},
)
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
connections = runner.invoke(app, [*base_args, "admin", "connections"])
statuses = runner.invoke(app, [*base_args, "admin", "statuses"])
events = runner.invoke(app, [*base_args, "admin", "events"])
assert connections.exit_code == 0, connections.output
assert '"connections": []' in connections.output
assert statuses.exit_code == 0, statuses.output
assert '"statuses": []' in statuses.output
assert events.exit_code == 0, events.output
assert '"kind": "workflow_test_event"' in events.output
def test_wf_remote_draft_artifact_deploy_lifecycle(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"remote_ws",
"--capability",
"wf.std.constant",
"--name",
"remote_constant",
"--title",
"Remote Constant",
],
)
assert created.exit_code == 0, created.output
assert '"workspace_id": "remote_ws"' in created.output
validated = runner.invoke(
app,
[*base_args, "draft", "validate", "remote_ws"],
)
assert validated.exit_code == 0, validated.output
assert '"status": "valid"' in validated.output
invalid_created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"repair_ws",
"--capability",
"wf.std.constant",
"--name",
"repair_constant",
],
)
assert invalid_created.exit_code == 0, invalid_created.output
invalid_patch = runner.invoke(
app,
[
*base_args,
"draft",
"set-output",
"repair_ws",
"--revision",
"1",
"--step",
"call",
"--map",
"value=state.missing",
],
)
assert invalid_patch.exit_code == 0, invalid_patch.output
invalid_validated = runner.invoke(
app,
[*base_args, "draft", "validate", "repair_ws"],
)
assert invalid_validated.exit_code == 0, invalid_validated.output
assert '"status": "invalid"' in invalid_validated.output
assert "bind repair_ws --revision 2" in invalid_validated.output
assert (
"--step call --from local.value --to state.missing" in invalid_validated.output
)
saved_artifact = runner.invoke(
app,
[
*base_args,
"draft",
"save",
"remote_ws",
"--artifact",
"remote_artifact",
"--version",
"1",
"--title",
"Remote Artifact",
"--outcome",
"ok",
],
)
assert saved_artifact.exit_code == 0, saved_artifact.output
assert '"artifact_id": "remote_artifact"' in saved_artifact.output
inspected_artifact = runner.invoke(
app,
[*base_args, "artifact", "inspect", "remote_artifact", "1"],
)
assert inspected_artifact.exit_code == 0, inspected_artifact.output
assert '"id": "remote_artifact"' in inspected_artifact.output
saved_deployment = runner.invoke(
app,
[
*base_args,
"deploy",
"save",
"remote_artifact.default",
"--artifact",
"remote_artifact",
"--version",
"1",
],
)
assert saved_deployment.exit_code == 0, saved_deployment.output
assert '"deployment_id": "remote_artifact.default"' in saved_deployment.output
validated_deployment = runner.invoke(
app,
[*base_args, "deploy", "validate", "remote_artifact.default"],
)
assert validated_deployment.exit_code == 0, validated_deployment.output
assert '"status": "runnable"' in validated_deployment.output
def test_wf_remote_run_resume_interrupted_deployment(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
asyncio.run(
server.api.create_artifact_from_plan(
artifact_id="remote_approval",
version=1,
title="Remote Approval",
plan=_interrupt_plan(),
outcomes=("submitted",),
)
)
asyncio.run(
server.api.save_deployment(
{
"id": "remote_approval.default",
"artifact_id": "remote_approval",
"artifact_version": 1,
"bindings": [],
}
)
)
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
started = runner.invoke(
app,
[
*base_args,
"run",
"start",
"remote_approval.default",
"--input",
'{"message": "approve?"}',
],
)
assert started.exit_code == 0, started.output
started_payload = json.loads(started.output)
assert started_payload["status"] == "interrupted"
resumed = runner.invoke(
app,
[
*base_args,
"run",
"resume",
started_payload["run_id"],
"--payload",
"{}",
],
)
assert resumed.exit_code == 0, resumed.output
resumed_payload = json.loads(resumed.output)
assert resumed_payload["run_id"] == started_payload["run_id"]
assert resumed_payload["status"] == "completed"
assert resumed_payload["outcome"] == "submitted"
def test_wf_status_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
asyncio.run(
server.api.create_artifact_from_plan(
artifact_id="status_constant",
version=1,
title="Status Constant",
plan=_constant_plan(),
outcomes=("ok",),
source_bindings={},
)
)
asyncio.run(
server.api.save_deployment(
{
"id": "status_constant.default",
"artifact_id": "status_constant",
"artifact_version": 1,
"bindings": {},
}
)
)
started = asyncio.run(
server.api.run_deployment(
deployment_id="status_constant.default",
workflow_input={},
)
)
_patch_rpc_client_to_server(monkeypatch, server)
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",
"status",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["target"]["mode"] == "remote"
assert payload["target"]["url"] == "http://test/rpc"
assert payload["workflow"]["capability_count"] >= 1
assert payload["runs"]["available"] is True
assert payload["runs"]["total"] == 1
assert payload["runs"]["completed"] == 1
assert payload["runs"]["failed"] == 0
assert payload["runs"]["interrupted"] == 0
assert payload["runs"]["latest"]["run_id"] == started["run_id"]
assert payload["runs"]["latest"]["status"] == "completed"
assert payload["sources"]["available"] is True
assert payload["admin"]["available"] is True
assert payload["registry"]["available"] is False
def test_wf_status_reports_rpc_config_target(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text(
json.dumps(
{
"version": 1,
"client": {
"target": {
"kind": "rpc_http",
"url": "http://test/rpc",
}
},
}
),
encoding="utf-8",
)
result = CliRunner().invoke(
app,
[
"--config",
str(config_path),
"status",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["target"]["mode"] == "remote"
assert payload["target"]["url"] == "http://test/rpc"
assert payload["workflow"]["capability_count"] >= 1
def test_wf_draft_delete_requires_confirm(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
result = runner.invoke(app, [*base_args, "draft", "delete", "delete-me"])
assert result.exit_code != 0
assert "confirm" in (result.output).lower()
def test_wf_draft_delete_succeeds_with_confirm(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"delete-me",
"--capability",
"wf.std.constant",
"--name",
"delete_me_ws",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(
app, [*base_args, "draft", "delete", "delete-me", "--confirm"]
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["workspace_id"] == "delete-me"
assert payload["deleted"] is True
def test_wf_source_diagnose_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
result = runner.invoke(app, [*base_args, "source", "diagnose", "wf.std"])
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["source_id"] == "wf.std"
assert payload["status"] == "unknown"
def test_wf_local_uses_selected_config_sources(tmp_path: Path) -> None:
config_path = write_python_source_config(tmp_path)
result = CliRunner().invoke(
app,
[
"--config",
str(config_path),
"--local",
"cap",
"list",
"--source",
"local.ops",
"--limit",
"100",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert {capability["name"] for capability in payload["capabilities"]} == {
"local.ops.echo"
}
def test_wf_draft_create_reports_optional_inputs_without_binding(
tmp_path: Path,
) -> None:
config_path = write_python_source_config(tmp_path)
runner = CliRunner()
base_args = ["--config", str(config_path), "--local"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"echo_ws",
"--capability",
"local.ops.echo",
],
)
inspected = runner.invoke(
app,
[*base_args, "draft", "inspect", "echo_ws", "--include-draft"],
)
assert created.exit_code == 0, created.output
assert inspected.exit_code == 0, inspected.output
created_payload = json.loads(created.output)
draft = json.loads(inspected.output)["draft"]
assert created_payload["wrapper_hints"]["input_map"] == {"input.text": "text"}
assert draft["steps"]["call"]["input"] == [{"path": "input.text", "target": "text"}]
assert any("path" in note for note in created_payload["wrapper_hints"]["notes"])
def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"focused_ws",
"--capability",
"wf.std.constant",
"--name",
"focused_initial",
],
)
assert created.exit_code == 0, created.output
named = runner.invoke(
app,
[
*base_args,
"draft",
"set-name",
"focused_ws",
"--revision",
"1",
"--name",
"focused_renamed",
],
)
routed = runner.invoke(
app,
[
*base_args,
"draft",
"set-route",
"focused_ws",
"--revision",
"2",
"--step",
"call",
"--outcome",
"ok",
"--to",
"__end__",
],
)
input_mapped = runner.invoke(
app,
[
*base_args,
"draft",
"set-input",
"focused_ws",
"--revision",
"3",
"--step",
"call",
"--map",
"input.value=value",
],
)
output_mapped = runner.invoke(
app,
[
*base_args,
"draft",
"set-output",
"focused_ws",
"--revision",
"4",
"--step",
"call",
"--map",
"value=state.value",
],
)
input_merged = runner.invoke(
app,
[
*base_args,
"draft",
"set-input",
"focused_ws",
"--revision",
"5",
"--step",
"call",
"--map",
"input.extra=extra",
"--merge",
],
)
output_merged = runner.invoke(
app,
[
*base_args,
"draft",
"set-output",
"focused_ws",
"--revision",
"6",
"--step",
"call",
"--map",
"extra=state.extra",
"--merge",
],
)
inspected = runner.invoke(
app,
[*base_args, "draft", "inspect", "focused_ws", "--include-draft"],
)
assert named.exit_code == 0, named.output
assert routed.exit_code == 0, routed.output
assert input_mapped.exit_code == 0, input_mapped.output
assert output_mapped.exit_code == 0, output_mapped.output
assert input_merged.exit_code == 0, input_merged.output
assert output_merged.exit_code == 0, output_merged.output
assert inspected.exit_code == 0, inspected.output
payload = json.loads(inspected.output)
draft = payload["draft"]
assert draft["name"] == "focused_renamed"
assert draft["routes"]["call"]["ok"] == "__end__"
assert draft["steps"]["call"]["input"] == [
{
"target": "value",
"path": "input.value",
},
{
"target": "extra",
"path": "input.extra",
},
]
assert draft["steps"]["call"]["output"] == [
{
"source": "value",
"target": "state.value",
},
{
"source": "extra",
"target": "state.extra",
},
]
def test_wf_draft_set_workflow_output_uses_rpc_target(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"report",
"--capability",
"wf.std.constant",
],
)
result = runner.invoke(
app,
[
*base_args,
"draft",
"set-workflow-output",
"report",
"--revision",
"1",
"--map",
"state.markdown=markdown",
],
)
inspected = runner.invoke(
app,
[*base_args, "draft", "inspect", "report", "--include-draft"],
)
assert created.exit_code == 0, created.output
assert result.exit_code == 0, result.output
assert inspected.exit_code == 0, inspected.output
draft = json.loads(inspected.output)["draft"]
assert draft["output"] == [{"path": "state.markdown", "target": "markdown"}]
def test_wf_draft_remove_route_uses_rpc_target(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"remove_route_ws",
"--capability",
"wf.std.constant",
"--name",
"remove_route",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(
app,
[
*base_args,
"draft",
"remove-route",
"remove_route_ws",
"--revision",
"1",
"--step",
"call",
"--outcome",
"ok",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["revision"] == 2
inspected = runner.invoke(
app,
[*base_args, "draft", "inspect", "remove_route_ws", "--include-draft"],
)
assert inspected.exit_code == 0, inspected.output
draft = json.loads(inspected.output)["draft"]
assert "ok" not in draft["routes"]["call"]
def test_wf_draft_bind_uses_rpc_target(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"snapshot_ws",
"--capability",
"wf.std.constant",
"--name",
"snapshot",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(
app,
[
*base_args,
"draft",
"bind",
"snapshot_ws",
"--revision",
"1",
"--step",
"call",
"--from",
"local.value",
"--to",
"state.result",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["revision"] == 2
inspected = runner.invoke(
app,
[*base_args, "draft", "inspect", "snapshot_ws", "--include-draft"],
)
assert inspected.exit_code == 0, inspected.output
draft = json.loads(inspected.output)["draft"]
assert draft["steps"]["call"]["output"] == [
{"source": "value", "target": "state.result"}
]
def test_wf_draft_add_step_from_capability_uses_rpc_target(
monkeypatch, tmp_path
) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"add_step_ws",
"--capability",
"wf.std.constant",
"--name",
"add_step",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(
app,
[
*base_args,
"draft",
"add-step",
"add_step_ws",
"--revision",
"1",
"--step",
"second",
"--capability",
"wf.std.constant",
"--from-step",
"call",
"--from-outcome",
"ok",
"--route",
"ok=__end__",
"--input",
"input.value=value",
"--bind-output",
"value=state.second_value",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["revision"] == 2
assert payload["status"] == "valid"
def test_wf_draft_add_step_reports_bare_output_target_without_traceback(
monkeypatch, tmp_path
) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"add_step_ws",
"--capability",
"wf.std.constant",
"--name",
"add_step",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(
app,
[
*base_args,
"draft",
"add-step",
"add_step_ws",
"--revision",
"1",
"--step",
"second",
"--capability",
"wf.std.constant",
"--bind-output",
"value=value",
],
)
assert result.exit_code != 0
assert "Traceback" not in result.output
assert "--bind-output" in result.output
assert "state.value" in result.output
def test_wf_draft_compile_prints_compiled_plan(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"compile_ws",
"--capability",
"wf.std.constant",
"--name",
"compile_me",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(app, [*base_args, "draft", "compile", "compile_ws"])
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["name"] == "compile_me"
assert "compiled_plan" not in payload
def test_wf_draft_compile_invalid_prints_diagnostics_to_stderr(
monkeypatch, tmp_path
) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
asyncio.run(
server.api.create_draft_workspace(
workspace_id="invalid_compile_ws",
draft={
"name": "invalid_compile",
"input_schema": {"type": "object"},
"state_schema": {"type": "object", "properties": {}},
"output_schema": {"type": "object", "properties": {}},
"start": "call",
"steps": {
"call": {
"use": "wf.std.constant",
"input": [],
"output": [],
}
},
"routes": {"call": {"typo": "__end__"}},
},
)
)
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
result = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"draft",
"compile",
"invalid_compile_ws",
],
)
assert result.exit_code == 1
# This Typer test runner mixes stderr into output; the command implementation
# writes invalid compile diagnostics with err=True for real terminals.
assert '"status": "invalid"' in result.output
assert "compiled_plan" not in result.output
def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
asyncio.run(
server.api.create_artifact_from_plan(
artifact_id="alias_artifact",
version=1,
title="Alias Artifact",
plan=_constant_plan(),
outcomes=("ok",),
)
)
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
created = runner.invoke(
app,
[
"--config",
str(config_path),
"--url",
"http://test/rpc",
"deploy",
"create",
"alias_artifact.default",
"--artifact",
"alias_artifact",
"--version",
"1",
],
)
assert created.exit_code == 0, created.output
payload = json.loads(created.output)
assert payload["deployment_id"] == "alias_artifact.default"
def test_wf_draft_forward_route_invalid_via_rpc(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"fwd_ws",
"--capability",
"wf.std.constant",
"--name",
"forward_route",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(
app,
[
*base_args,
"draft",
"add-step",
"fwd_ws",
"--revision",
"1",
"--step",
"second",
"--capability",
"wf.std.constant",
"--from-step",
"call",
"--route",
"ok=missing",
"--input",
"input.value=value",
"--bind-output",
"value=state.val",
],
)
assert result.exit_code == 0, result.output
payload = json.loads(result.output)
assert payload["status"] == "invalid"
assert payload["revision"] == 2