docs: record workflow config target slice
This commit is contained in:
@@ -93,6 +93,9 @@ implementation state.
|
||||
- Completed: the first JSON-RPC-over-HTTP transport can expose the local/static
|
||||
`WorkflowServer` through fixed dotted methods. Remote CLI targeting remains
|
||||
the next transport-facing slice.
|
||||
- Completed: workflow config now distinguishes client targets from server
|
||||
hosting config, and selected `wf` commands can target JSON-RPC HTTP with
|
||||
explicit CLI overrides.
|
||||
|
||||
5. **CLI/API alignment**
|
||||
- Let the CLI target either local process-backed stores/runtime or the future
|
||||
|
||||
@@ -452,6 +452,26 @@ Relative paths should resolve relative to the config file directory.
|
||||
- Do not derive identity by splitting dotted source ids.
|
||||
- Do not use MCP as the first-class `wf` CLI target.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
First slice implemented:
|
||||
|
||||
- neutral `wf_config` models and loader
|
||||
- filesystem server store config
|
||||
- stdlib source bootstrap config
|
||||
- local and JSON-RPC client targets
|
||||
- `wf` root overrides for `--local`, `--url`, and `--timeout`
|
||||
- remote JSON-RPC client support for capability and run CLI commands
|
||||
- `wf-rpc-server --config` support for server store and RPC HTTP transport
|
||||
|
||||
Still future:
|
||||
|
||||
- store-backed mutable source registry
|
||||
- MCP/OpenAPI source config
|
||||
- `/mcp` hosting from neutral server config
|
||||
- remote draft/artifact/deployment CLI commands
|
||||
- auth and SQL stores
|
||||
|
||||
## Next Implementation Slice
|
||||
|
||||
First config implementation should be small:
|
||||
|
||||
@@ -9,7 +9,12 @@ from wf_api import WorkflowApi
|
||||
from wf_mcp.broker import build_service_from_config, load_broker_config
|
||||
from wf_mcp.broker.service import WfMcpService
|
||||
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
||||
from wf_config import FilesystemStoreConfig, LocalTargetConfig, RpcHttpTargetConfig, load_workflow_config
|
||||
from wf_config import (
|
||||
FilesystemStoreConfig,
|
||||
LocalTargetConfig,
|
||||
RpcHttpTargetConfig,
|
||||
load_workflow_config,
|
||||
)
|
||||
from wf_server import build_local_static_workflow_server
|
||||
from wf_transport_rpc_http import RpcWorkflowApiClient
|
||||
|
||||
@@ -20,7 +25,7 @@ class CliContext:
|
||||
|
||||
config_path: Path
|
||||
service: WfMcpService | None
|
||||
handlers: WorkflowApi | RpcWorkflowApiClient
|
||||
handlers: "WorkflowApi | RpcWorkflowApiClient"
|
||||
|
||||
|
||||
def config_path_from_context(ctx: typer.Context) -> str:
|
||||
|
||||
@@ -2,10 +2,14 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pathlib import Path
|
||||
import typer
|
||||
import uvicorn
|
||||
from wf_config import FilesystemStoreConfig, RpcHttpTransportConfig, load_workflow_config
|
||||
|
||||
from wf_config import (
|
||||
FilesystemStoreConfig,
|
||||
RpcHttpTransportConfig,
|
||||
load_workflow_config,
|
||||
)
|
||||
|
||||
from wf_server import build_local_static_workflow_server
|
||||
|
||||
@@ -37,7 +41,9 @@ def serve(
|
||||
workflow_config = load_workflow_config(config)
|
||||
store = workflow_config.server.store
|
||||
if not isinstance(store, FilesystemStoreConfig):
|
||||
raise typer.BadParameter("wf-rpc-server currently requires filesystem store")
|
||||
raise typer.BadParameter(
|
||||
"wf-rpc-server currently requires filesystem store"
|
||||
)
|
||||
resolved_store_root = resolved_store_root or store.root
|
||||
rpc_transport = next(
|
||||
(
|
||||
@@ -51,7 +57,9 @@ def serve(
|
||||
resolved_host = host or rpc_transport.host
|
||||
resolved_port = port or rpc_transport.port
|
||||
if resolved_store_root is None:
|
||||
raise typer.BadParameter("--store-root is required when --config is not supplied")
|
||||
raise typer.BadParameter(
|
||||
"--store-root is required when --config is not supplied"
|
||||
)
|
||||
|
||||
server = build_local_static_workflow_server(resolved_store_root)
|
||||
rpc_app = create_rpc_app(server)
|
||||
|
||||
@@ -55,20 +55,16 @@ def test_context_from_service_exposes_existing_store_objects(tmp_path: Path) ->
|
||||
encoding="utf-8",
|
||||
)
|
||||
cli_context = load_cli_context(config_path)
|
||||
service = cli_context.service
|
||||
assert service is not None
|
||||
|
||||
operation_context = context_from_service(cli_context.service)
|
||||
operation_context = context_from_service(service)
|
||||
|
||||
assert isinstance(operation_context, WorkflowOperationContext)
|
||||
assert operation_context.artifact_store is cli_context.service.artifact_store
|
||||
assert (
|
||||
operation_context.draft_workspace_store
|
||||
is cli_context.service.draft_workspace_store
|
||||
)
|
||||
assert operation_context.run_store is cli_context.service.run_store
|
||||
assert (
|
||||
operation_context.specs.capability_sources
|
||||
is cli_context.service.capability_sources
|
||||
)
|
||||
assert operation_context.artifact_store is service.artifact_store
|
||||
assert operation_context.draft_workspace_store is service.draft_workspace_store
|
||||
assert operation_context.run_store is service.run_store
|
||||
assert operation_context.specs.capability_sources is service.capability_sources
|
||||
|
||||
|
||||
def test_context_from_service_delegates_specs_and_events(tmp_path: Path) -> None:
|
||||
@@ -78,12 +74,14 @@ def test_context_from_service_delegates_specs_and_events(tmp_path: Path) -> None
|
||||
encoding="utf-8",
|
||||
)
|
||||
cli_context = load_cli_context(config_path)
|
||||
operation_context = context_from_service(cli_context.service)
|
||||
service = cli_context.service
|
||||
assert service is not None
|
||||
operation_context = context_from_service(service)
|
||||
|
||||
event = object()
|
||||
operation_context.events.record_event(event)
|
||||
|
||||
assert cli_context.service.list_events()[-1] is event
|
||||
assert service.list_events()[-1] is event
|
||||
|
||||
|
||||
def test_context_from_service_record_workflow_event(tmp_path: Path) -> None:
|
||||
@@ -93,7 +91,9 @@ def test_context_from_service_record_workflow_event(tmp_path: Path) -> None:
|
||||
encoding="utf-8",
|
||||
)
|
||||
cli_context = load_cli_context(config_path)
|
||||
operation_context = context_from_service(cli_context.service)
|
||||
service = cli_context.service
|
||||
assert service is not None
|
||||
operation_context = context_from_service(service)
|
||||
|
||||
operation_context.events.record_workflow_event(
|
||||
"workflow_artifact_saved",
|
||||
@@ -101,7 +101,7 @@ def test_context_from_service_record_workflow_event(tmp_path: Path) -> None:
|
||||
payload={"artifact_id": "demo", "version": 1},
|
||||
)
|
||||
|
||||
recorded = cli_context.service.events.list_events()[-1]
|
||||
recorded = service.events.list_events()[-1]
|
||||
assert recorded.kind == "workflow_artifact_saved"
|
||||
assert recorded.capability_id == "workflow.demo.v1"
|
||||
assert recorded.payload["artifact_id"] == "demo"
|
||||
|
||||
@@ -27,12 +27,13 @@ def test_load_cli_context_builds_service_and_handlers(tmp_path: Path) -> None:
|
||||
)
|
||||
|
||||
context = load_cli_context(config_path)
|
||||
service = context.service
|
||||
assert service is not None
|
||||
|
||||
assert context.config_path == config_path
|
||||
assert context.service.connections.list_all()[0].id == "demo.personal"
|
||||
assert context.handlers.context.artifact_store is context.service.artifact_store
|
||||
assert service.connections.list_all()[0].id == "demo.personal"
|
||||
assert context.handlers.context.artifact_store is service.artifact_store
|
||||
assert (
|
||||
context.handlers.context.draft_workspace_store
|
||||
is context.service.draft_workspace_store
|
||||
context.handlers.context.draft_workspace_store is service.draft_workspace_store
|
||||
)
|
||||
assert context.handlers.context.run_store is context.service.run_store
|
||||
assert context.handlers.context.run_store is service.run_store
|
||||
|
||||
@@ -9,7 +9,9 @@ from typer.testing import CliRunner
|
||||
|
||||
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
|
||||
from wf_cli.app import app
|
||||
from wf_cli.context import CliContext, load_cli_context
|
||||
from typer import Context as TyperContext
|
||||
|
||||
from wf_cli.context import CliContext, config_path_from_context, load_cli_context
|
||||
from wf_cli.formats import ListOutputFormat, render_list_payload
|
||||
from wf_cli.io import CliInputError, parse_bindings, parse_json_value
|
||||
|
||||
@@ -17,6 +19,18 @@ from tests.wf_mcp.test_support import echo_tool, local_temp_root
|
||||
from tests.wf_mcp.workflow_surface.conftest import echo_artifact
|
||||
|
||||
|
||||
def _load_cli_context_with_specs(ctx: TyperContext | str | Path) -> CliContext:
|
||||
if isinstance(ctx, (str, Path)):
|
||||
config_path = ctx
|
||||
else:
|
||||
config_path = config_path_from_context(ctx)
|
||||
context = load_cli_context(config_path)
|
||||
service = context.service
|
||||
assert service is not None
|
||||
service.register_specs("demo.personal", echo_tool)
|
||||
return context
|
||||
|
||||
|
||||
def test_render_list_payload_ids_uses_requested_id_field() -> None:
|
||||
payload = {"capabilities": [{"name": "wf.std.truthy"}, {"name": "wf.std.add"}]}
|
||||
|
||||
@@ -132,30 +146,14 @@ def _write_cli_config(root: Path) -> Path:
|
||||
return config_path
|
||||
|
||||
|
||||
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
|
||||
in-memory test NodeSpecs. Production source registration remains outside
|
||||
this CLI slice.
|
||||
"""
|
||||
context = load_cli_context(config_path)
|
||||
context.service.register_specs("demo.personal", echo_tool)
|
||||
return context
|
||||
|
||||
|
||||
def test_wf_cap_list_outputs_json() -> None:
|
||||
root = local_temp_root() / "wf_cli_cap_list"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
config_path = _write_cli_config(root)
|
||||
|
||||
with patch("wf_cli.commands.caps.load_cli_context_from_typer", _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"],
|
||||
@@ -172,7 +170,9 @@ 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_from_typer", _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,
|
||||
[
|
||||
@@ -196,7 +196,9 @@ 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_from_typer", _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"],
|
||||
|
||||
@@ -2,8 +2,15 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import httpx
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wf_api.models import RawWorkflowPlan
|
||||
from wf_cli.app import app
|
||||
from wf_cli.context import load_cli_context
|
||||
from wf_transport_rpc_http import RpcWorkflowApiClient
|
||||
from wf_core import END
|
||||
from wf_server import build_local_static_workflow_server
|
||||
from wf_transport_rpc_http import RpcWorkflowApiClient, create_rpc_app
|
||||
|
||||
|
||||
def test_load_cli_context_uses_rpc_client_for_rpc_http_target(tmp_path) -> None:
|
||||
@@ -57,16 +64,6 @@ 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:
|
||||
@@ -124,7 +121,9 @@ def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
original_client = httpx.AsyncClient
|
||||
monkeypatch.setattr(
|
||||
"wf_transport_rpc_http.client.httpx.AsyncClient",
|
||||
lambda *args, **kwargs: original_client(transport=transport, base_url="http://test"),
|
||||
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")
|
||||
|
||||
@@ -8,7 +8,9 @@ from typer.testing import CliRunner
|
||||
|
||||
from wf_artifacts import FileWorkflowArtifactStore, WorkflowDeployment
|
||||
from wf_cli.app import app
|
||||
from wf_cli.context import CliContext, load_cli_context
|
||||
from typer import Context as TyperContext
|
||||
|
||||
from wf_cli.context import CliContext, config_path_from_context, load_cli_context
|
||||
|
||||
from tests.wf_mcp.test_support import echo_tool, local_temp_root
|
||||
from tests.wf_mcp.workflow_surface.conftest import echo_artifact
|
||||
@@ -17,6 +19,25 @@ from tests.wf_mcp.workflow_surface.conftest import echo_artifact
|
||||
runner = CliRunner()
|
||||
|
||||
|
||||
def _load_cli_context_with_specs(ctx: TyperContext | str | Path) -> CliContext:
|
||||
"""Test-only hook: seed executable demo specs for CLI integration tests.
|
||||
|
||||
``build_service_from_config`` registers connections, adapters, and
|
||||
file-backed stores, but not in-memory node specs. Production code does
|
||||
not auto-register specs; this helper exists solely so CLI tests can
|
||||
exercise the full command path with a runnable deployment.
|
||||
"""
|
||||
if isinstance(ctx, (str, Path)):
|
||||
config_path = ctx
|
||||
else:
|
||||
config_path = config_path_from_context(ctx)
|
||||
context = load_cli_context(config_path)
|
||||
service = context.service
|
||||
assert service is not None
|
||||
service.register_specs("demo.personal", echo_tool)
|
||||
return context
|
||||
|
||||
|
||||
def _write_config(root: Path) -> Path:
|
||||
config_path = root / "wf_mcp.config.json"
|
||||
config_path.write_text(
|
||||
@@ -53,25 +74,6 @@ def _seed_echo_deployment(root: Path) -> Path:
|
||||
return config_path
|
||||
|
||||
|
||||
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
|
||||
file-backed stores, but not in-memory node specs. Production code does
|
||||
not auto-register specs; this helper exists solely so CLI tests can
|
||||
exercise the full command path with a runnable deployment.
|
||||
"""
|
||||
context = load_cli_context(config_path)
|
||||
context.service.register_specs("demo.personal", echo_tool)
|
||||
return context
|
||||
|
||||
|
||||
def test_wf_deploy_validate_outputs_json() -> None:
|
||||
root = local_temp_root() / "wf_cli_deploy_validate"
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
@@ -99,7 +101,9 @@ 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_from_typer", _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,
|
||||
[
|
||||
@@ -128,7 +132,9 @@ 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_from_typer", _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,
|
||||
[
|
||||
@@ -153,7 +159,9 @@ 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_from_typer", _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,
|
||||
[
|
||||
@@ -206,7 +214,9 @@ 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_from_typer", _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,
|
||||
[
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -10,6 +12,7 @@ from wf_config import (
|
||||
RpcHttpTransportConfig,
|
||||
StdlibSourceConfig,
|
||||
WorkflowConfigFile,
|
||||
load_workflow_config,
|
||||
)
|
||||
|
||||
|
||||
@@ -87,9 +90,6 @@ def test_workflow_config_rejects_unknown_target_kind() -> None:
|
||||
"client": {"target": {"kind": "mcp"}},
|
||||
}
|
||||
)
|
||||
import json
|
||||
|
||||
from wf_config import load_workflow_config
|
||||
|
||||
|
||||
def test_load_workflow_config_resolves_filesystem_store_relative_to_config(
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from wf_transport_rpc_http.cli import app
|
||||
@@ -12,7 +14,6 @@ def test_rpc_server_cli_help_mentions_store_root() -> None:
|
||||
assert "--store-root" in result.output
|
||||
assert "--host" in result.output
|
||||
assert "--port" in result.output
|
||||
import json
|
||||
|
||||
|
||||
def test_rpc_server_cli_accepts_config_file(tmp_path) -> None:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
Reference in New Issue
Block a user