From b377311a6e600e07641dfd2eebdd3f75ef3d6bb5 Mon Sep 17 00:00:00 2001 From: lda Date: Mon, 31 Aug 2026 14:07:53 +0700 Subject: [PATCH] fix: opt draft storage into real callers --- .../python-workflow-client/task-7-report.md | 24 ++++++++++++++++ docs/wf_api_architecture.md | 1 + src/wf_api/stores.py | 15 ++++++++-- src/wf_cli/context.py | 7 +++-- src/wf_mcp/broker/config.py | 4 ++- src/wf_mcp/broker/server.py | 3 +- src/wf_mcp/workflow_surface/handlers.py | 6 ++-- src/wf_mcp/workflow_surface/tools.py | 4 ++- src/wf_server/cli.py | 12 ++++++-- src/wf_server/config.py | 7 ++++- src/wf_server/context.py | 2 +- tests/wf_api/test_artifact_api.py | 4 +-- tests/wf_api/test_stores.py | 18 ++++++++++-- tests/wf_cli/test_context.py | 20 ++++--------- tests/wf_mcp/server/test_tools.py | 6 ++-- tests/wf_mcp/test_mcp_workflow_server.py | 2 ++ tests/wf_server/test_cli.py | 28 +++++++++++++------ tests/wf_server/test_local_static_server.py | 26 +++++++++++++++-- tests/wf_transport_rpc_http/test_app.py | 28 +++++++++++++++++++ tests/wf_transport_rpc_http/test_client.py | 20 +++++++++++++ .../test_openrpc_contract.py | 7 ++++- 21 files changed, 194 insertions(+), 50 deletions(-) diff --git a/.superpowers/sdd/python-workflow-client/task-7-report.md b/.superpowers/sdd/python-workflow-client/task-7-report.md index f8c380a1..8fc93e0c 100644 --- a/.superpowers/sdd/python-workflow-client/task-7-report.md +++ b/.superpowers/sdd/python-workflow-client/task-7-report.md @@ -32,6 +32,30 @@ explicit `drafts=True` opt-in, and one thesis asset test expects untracked PDF figures absent from the base worktree. No generated `.wf_mcp_store/` or `test-artifacts/` files are part of this change. +## Fix round 2 + +- `file_workflow_stores()` now skips `FileDraftWorkspaceStore` by default; + `build_local_static_workflow_server()` forwards `drafts` so default local + composition creates no draft directory, while `drafts=True` remains a + working explicit opt-in. +- Draft-bearing MCP broker/workflow-surface and CLI compositions now pass + `drafts=True` explicitly. The standalone RPC server CLI does the same for + its documented draft RPC surface. The complete OpenRPC inventory fixture and + draft-focused RPC tests now opt into both server storage and RPC method + registration. +- The API architecture walkthrough now imports `App` from `wf_client`. + +Fix-round 2 verification (fresh after formatting): + +- `uv run pytest tests/wf_api/test_stores.py tests/wf_server/test_local_static_server.py tests/wf_mcp/test_mcp_workflow_server.py tests/wf_mcp/server/test_tools.py tests/wf_mcp/workflow_surface tests/wf_cli/test_context.py tests/wf_server/test_cli.py tests/wf_transport_rpc_http/test_openrpc_contract.py -q` — 183 passed. +- `uv run pytest tests/wf_client tests/authoring/test_builder.py tests/authoring/test_subgraph.py tests/wf_api/test_artifact_api.py tests/wf_transport_rpc_http/test_client.py tests/wf_transport_rpc_http/test_app.py tests/wf_transport_rpc_http/test_openrpc_contract.py tests/wf_contract_manifest/test_generate.py tests/wf_contract_manifest/test_committed_manifest.py -q` — 291 passed, 201 warnings. +- Ruff check and `ruff format --check` on changed Python surfaces — passed. +- `uv run basedpyright --level error src/wf_api src/wf_cli src/wf_mcp/broker src/wf_mcp/workflow_surface src/wf_server` — 0 errors, 0 warnings, 0 notes. +- `uv run python -m wf_contract_manifest check` — passed. +- `pnpm --dir web --filter @lda/workflow-rpc contract:check` — passed. +- `pnpm --dir web --filter @lda/workflow-rpc test` — 151 passed, 3 skipped. +- `git diff --check` — passed. + ## Fix round 1 - Normal `WorkflowApi`, nested capability/artifact services, durable context, diff --git a/docs/wf_api_architecture.md b/docs/wf_api_architecture.md index 14a42550..0c18acc1 100644 --- a/docs/wf_api_architecture.md +++ b/docs/wf_api_architecture.md @@ -114,6 +114,7 @@ Hypothetically, an application that wants to turn a discovered capability into a durable run would use the following complete flow: ```python +from wf_client import App from wf_authoring import input_from, input_value, output_to, state_path app = App.from_http_jsonrpc("http://localhost:8765/rpc") diff --git a/src/wf_api/stores.py b/src/wf_api/stores.py index 68507a58..8d3cb34d 100644 --- a/src/wf_api/stores.py +++ b/src/wf_api/stores.py @@ -22,12 +22,21 @@ class WorkflowStores: run_store: RunStore -def file_workflow_stores(root: str | Path) -> WorkflowStores: - """Create process-local file-backed workflow stores under one root.""" +def file_workflow_stores( + root: str | Path, + *, + drafts: bool = False, +) -> WorkflowStores: + """Create file-backed workflow stores, opting into draft persistence. + + Artifact and run stores are needed by every durable workflow server. Draft + workspaces are a separate product surface, so avoid constructing their + store (which creates its directory) unless a caller explicitly enables it. + """ store_root = Path(root) return WorkflowStores( artifact_store=FileWorkflowArtifactStore(store_root), - draft_workspace_store=FileDraftWorkspaceStore(store_root), + draft_workspace_store=(FileDraftWorkspaceStore(store_root) if drafts else None), run_store=FileRunStore(store_root), ) diff --git a/src/wf_cli/context.py b/src/wf_cli/context.py index ec5e3e68..76441472 100644 --- a/src/wf_cli/context.py +++ b/src/wf_cli/context.py @@ -97,7 +97,9 @@ def build_workflow_server_from_workflow_config( """Build the local server without importing the server runtime at CLI startup.""" from wf_server.config import build_workflow_server_from_workflow_config as build - return build(config) + # Local CLI exposes the full draft command group, so it is an explicit + # draft-bearing composition even though the neutral server default is not. + return build(config, drafts=True) def load_cli_context( @@ -150,7 +152,8 @@ def load_cli_context( return CliContext( config_path=resolved_config_path, service=service, - handlers=WorkflowApi(context_from_service(service)), + # Legacy MCP CLI commands include draft authoring operations. + handlers=WorkflowApi(context_from_service(service), drafts=True), source_admin=WorkflowSourceAdminApi(context_from_service(service)), admin=WorkflowAdminApi( connections=service.connection_service, diff --git a/src/wf_mcp/broker/config.py b/src/wf_mcp/broker/config.py index 087b08ed..b97d2664 100644 --- a/src/wf_mcp/broker/config.py +++ b/src/wf_mcp/broker/config.py @@ -163,7 +163,9 @@ def build_service_from_config(config: BrokerConfig) -> WfMcpService: """Create a broker service with SDK adapters for configured connections.""" runtime_factory = PersistentSessionFactory() store_roots = config.store_roots - workflow_stores = file_workflow_stores(store_roots.workflow_root) + # The broker's documented workflow/draft tools are a draft-bearing + # composition, so opt into the otherwise disabled draft store explicitly. + workflow_stores = file_workflow_stores(store_roots.workflow_root, drafts=True) # Keep FileStore as the compatibility facade on WfMcpService.store while # focused services receive role-specific stores. auth_store = FileAuthStore(store_roots.auth_root) diff --git a/src/wf_mcp/broker/server.py b/src/wf_mcp/broker/server.py index 0b258631..13c524d0 100644 --- a/src/wf_mcp/broker/server.py +++ b/src/wf_mcp/broker/server.py @@ -64,7 +64,8 @@ def workflow_server_from_service( raise ValueError("MCP-backed WorkflowServer requires workflow stores") context = context_from_service(service) - api: WorkflowApi = durable_workflow_api(context) + # MCP's documented workflow server includes mutable draft operations. + api: WorkflowApi = durable_workflow_api(context, drafts=True) source_diagnostics = SourceDiagnosticsProvider( connection_lookup=service.connections.get, auth_store=service.auth_store or service.store, diff --git a/src/wf_mcp/workflow_surface/handlers.py b/src/wf_mcp/workflow_surface/handlers.py index e3a70791..b99bc04d 100644 --- a/src/wf_mcp/workflow_surface/handlers.py +++ b/src/wf_mcp/workflow_surface/handlers.py @@ -13,14 +13,16 @@ if TYPE_CHECKING: class WorkflowSurfaceHandlers(WorkflowApi): """Compatibility wrapper for old wf_mcp.workflow_surface imports. - New code should construct `WorkflowApi(context_from_service(service))` + New code should construct `WorkflowApi(context_from_service(service), drafts=True)` directly. This shim keeps tests and legacy broker artifact tools working for legacy callers. """ def __init__(self, service: WfMcpService) -> None: self.service = service - super().__init__(context_from_service(service)) + # This compatibility surface exposes the draft methods used by the + # workflow-surface tests and legacy broker artifact tools. + super().__init__(context_from_service(service), drafts=True) __all__ = ["WorkflowSurfaceHandlers"] diff --git a/src/wf_mcp/workflow_surface/tools.py b/src/wf_mcp/workflow_surface/tools.py index f7f46979..7deda098 100644 --- a/src/wf_mcp/workflow_surface/tools.py +++ b/src/wf_mcp/workflow_surface/tools.py @@ -71,7 +71,9 @@ from .models import ( def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None: """Register stable workflow tools on the public MCP server surface.""" - handlers = WorkflowApi(context_from_service(service)) + # This MCP surface registers the draft authoring tools below, so its API + # composition must explicitly opt into the draft services. + handlers = WorkflowApi(context_from_service(service), drafts=True) @server.tool( name="wf.workflow.list_artifacts", diff --git a/src/wf_server/cli.py b/src/wf_server/cli.py index c682054f..54e344b6 100644 --- a/src/wf_server/cli.py +++ b/src/wf_server/cli.py @@ -68,7 +68,11 @@ def serve( workflow_config = load_workflow_config(config) store = workflow_config.server.workflow_store if server is None and store_root is None: - server = build_workflow_server_from_workflow_config(workflow_config) + # The server CLI exposes the full draft RPC surface. + server = build_workflow_server_from_workflow_config( + workflow_config, + drafts=True, + ) elif server is None: has_mcp_sources = any( getattr(source, "kind", None) == "mcp" @@ -101,9 +105,11 @@ def serve( raise typer.BadParameter( "--store-root is required when --config is not supplied" ) - server = build_local_static_workflow_server(resolved_store_root) + # The standalone server CLI is the documented draft-capable RPC + # endpoint; opt into its persistence explicitly at this boundary. + server = build_local_static_workflow_server(resolved_store_root, drafts=True) - rpc_app = create_rpc_app(server, rpc_path=resolved_rpc_path) + rpc_app = create_rpc_app(server, rpc_path=resolved_rpc_path, drafts=True) uvicorn.run( rpc_app, host=resolved_host or "127.0.0.1", diff --git a/src/wf_server/config.py b/src/wf_server/config.py index 8c3a9667..5bdcc42a 100644 --- a/src/wf_server/config.py +++ b/src/wf_server/config.py @@ -53,11 +53,15 @@ def _build_mcp_workflow_server_from_legacy_config(path: Path) -> WorkflowServer: def build_workflow_server_from_workflow_config( config: WorkflowConfigFile, + *, + drafts: bool = False, ) -> WorkflowServer: """Build a WorkflowServer from neutral workflow config. Local/static configs use built-in sources. Configs with ``kind: "mcp"`` - sources delegate to the MCP provider adapter. + sources delegate to the MCP provider adapter. Draft persistence is opt-in + for static composition; MCP broker composition owns its draft-bearing + workflow surface. """ if _has_mcp_sources(config): return _build_mcp_workflow_server_from_workflow_config(config) @@ -68,6 +72,7 @@ def build_workflow_server_from_workflow_config( raise ValueError("wf-rpc-server currently requires filesystem store") return build_local_static_workflow_server( store.root, + drafts=drafts, extra_sources=collect_static_sources(_static_source_providers(config)), ) diff --git a/src/wf_server/context.py b/src/wf_server/context.py index 31e09b30..0496fa05 100644 --- a/src/wf_server/context.py +++ b/src/wf_server/context.py @@ -299,7 +299,7 @@ def build_local_static_workflow_server( ) -> WorkflowServer: """Build a durable local/static server, with drafts as an explicit opt-in.""" config = WorkflowServerConfig(store_root=Path(root)) - stores = file_workflow_stores(config.store_root) + stores = file_workflow_stores(config.store_root, drafts=drafts) events = InMemoryWorkflowEventRecorder() sources = builtin_sources() if extra_sources: diff --git a/tests/wf_api/test_artifact_api.py b/tests/wf_api/test_artifact_api.py index f7aadb1f..d1a74494 100644 --- a/tests/wf_api/test_artifact_api.py +++ b/tests/wf_api/test_artifact_api.py @@ -131,7 +131,7 @@ def _artifact_api( ) service.register_specs("demo.personal", echo_tool) context = context_from_service(service) - return WorkflowArtifactApi(context), service + return WorkflowArtifactApi(context, drafts=True), service @pytest.mark.asyncio @@ -454,7 +454,7 @@ def _api(root: Path) -> WorkflowApi: artifact_store=FileWorkflowArtifactStore(root), draft_workspace_store=FileDraftWorkspaceStore(mcp_root), ) - return WorkflowApi(context_from_service(service)) + return WorkflowApi(context_from_service(service), drafts=True) @pytest.mark.asyncio diff --git a/tests/wf_api/test_stores.py b/tests/wf_api/test_stores.py index 5b3d8f5b..edbe94d1 100644 --- a/tests/wf_api/test_stores.py +++ b/tests/wf_api/test_stores.py @@ -10,18 +10,30 @@ from wf_artifacts import ( ) -def test_file_workflow_stores_constructs_all_three_file_stores(tmp_path: Path) -> None: +def test_file_workflow_stores_skips_draft_store_by_default(tmp_path: Path) -> None: root = tmp_path / "wf_api_file_workflow_stores" stores = file_workflow_stores(root) assert isinstance(stores, WorkflowStores) assert isinstance(stores.artifact_store, FileWorkflowArtifactStore) - assert isinstance(stores.draft_workspace_store, FileDraftWorkspaceStore) + assert stores.draft_workspace_store is None assert isinstance(stores.run_store, FileRunStore) assert stores.artifact_store.root == root - assert stores.draft_workspace_store.root == root assert stores.run_store.root == root + assert not (root / "draft_workspaces").exists() + + +def test_file_workflow_stores_constructs_draft_store_when_explicitly_enabled( + tmp_path: Path, +) -> None: + root = tmp_path / "wf_api_file_workflow_stores_drafts" + + stores = file_workflow_stores(root, drafts=True) + + assert isinstance(stores.draft_workspace_store, FileDraftWorkspaceStore) + assert stores.draft_workspace_store.root == root + assert (root / "draft_workspaces").is_dir() def test_wf_api_exports_workflow_stores() -> None: diff --git a/tests/wf_cli/test_context.py b/tests/wf_cli/test_context.py index 0a86f617..5571572a 100644 --- a/tests/wf_cli/test_context.py +++ b/tests/wf_cli/test_context.py @@ -16,7 +16,6 @@ from wf_cli.context import ( rpc_timeout_from_context, rpc_url_from_context, ) -from wf_server.config import build_workflow_server_from_workflow_config from .conftest import write_python_source_config @@ -104,7 +103,6 @@ def test_load_cli_context_builds_service_and_handlers(tmp_path: Path) -> None: def test_load_cli_context_local_uses_workflow_store_override( tmp_path: Path, - monkeypatch, ) -> None: config_path = tmp_path / "wf.json" config_path.write_text( @@ -125,21 +123,15 @@ def test_load_cli_context_local_uses_workflow_store_override( ), encoding="utf-8", ) - captured: dict[str, object] = {} - - def fake_build_workflow_server_from_workflow_config(config): - captured["store_root"] = config.server.workflow_store.root - return build_workflow_server_from_workflow_config(config) - - monkeypatch.setattr( - "wf_cli.context.build_workflow_server_from_workflow_config", - fake_build_workflow_server_from_workflow_config, - ) - context = load_cli_context(config_path) assert context.service is None - assert captured["store_root"] == (tmp_path / ".workflow").resolve() + assert isinstance(context.handlers, WorkflowApi) + assert context.handlers.drafts_enabled is True + assert ( + context.handlers.context.artifact_store.root + == (tmp_path / ".workflow").resolve() + ) @pytest.mark.asyncio diff --git a/tests/wf_mcp/server/test_tools.py b/tests/wf_mcp/server/test_tools.py index d81e700b..0096c418 100644 --- a/tests/wf_mcp/server/test_tools.py +++ b/tests/wf_mcp/server/test_tools.py @@ -101,7 +101,7 @@ async def test_registered_output_bindings_tool_delegates_typed_bindings_once( recorder = RecordingWorkflowHandler() monkeypatch.setattr( "wf_mcp.workflow_surface.tools.WorkflowApi", - lambda _context: recorder, + lambda _context, **_kwargs: recorder, ) service = WfMcpService( store=FileStore(tmp_path / "tool_invocation_store"), @@ -167,7 +167,7 @@ async def test_registered_workflow_output_bindings_tool_preserves_union_order( recorder = RecordingWorkflowHandler() monkeypatch.setattr( "wf_mcp.workflow_surface.tools.WorkflowApi", - lambda _context: recorder, + lambda _context, **_kwargs: recorder, ) service = WfMcpService( store=FileStore(tmp_path / "workflow_output_tool_store"), @@ -242,7 +242,7 @@ async def test_registered_capability_tools_delegate_presence_aware_requests( recorder = RecordingWorkflowHandler() monkeypatch.setattr( "wf_mcp.workflow_surface.tools.WorkflowApi", - lambda _context: recorder, + lambda _context, **_kwargs: recorder, ) service = WfMcpService( store=FileStore(tmp_path / "capability_tool_store"), diff --git a/tests/wf_mcp/test_mcp_workflow_server.py b/tests/wf_mcp/test_mcp_workflow_server.py index 2a72d61a..7aa89387 100644 --- a/tests/wf_mcp/test_mcp_workflow_server.py +++ b/tests/wf_mcp/test_mcp_workflow_server.py @@ -69,6 +69,8 @@ def test_workflow_server_from_service_wires_neutral_surfaces(tmp_path) -> None: assert isinstance(server, WorkflowServer) assert server.config.store_root == config.store_root assert server.api.context is server.context + assert server.api.drafts_enabled is True + assert server.api.drafts is not None assert server.source_registry_admin is not None assert server.admin.connections is service.connection_service assert server.admin.events is service.events diff --git a/tests/wf_server/test_cli.py b/tests/wf_server/test_cli.py index 527a8bbd..93a35869 100644 --- a/tests/wf_server/test_cli.py +++ b/tests/wf_server/test_cli.py @@ -83,13 +83,15 @@ def test_rpc_server_cli_uses_configured_store_and_transport( ) captured: dict[str, object] = {} - def fake_build_server(config): + def fake_build_server(config, *, drafts=False): captured["store_root"] = config.server.store.root + captured["drafts"] = drafts return object() - def fake_create_rpc_app(server, *, rpc_path="/rpc"): + def fake_create_rpc_app(server, *, rpc_path="/rpc", drafts=False): captured["server"] = server captured["rpc_path"] = rpc_path + captured["drafts"] = drafts return object() def fake_uvicorn_run(app_obj, *, host, port, access_log): @@ -110,6 +112,7 @@ def test_rpc_server_cli_uses_configured_store_and_transport( assert result.exit_code == 0, result.output assert captured["store_root"] == (tmp_path / ".wf_store").resolve() assert captured["rpc_path"] == "/workflow-rpc" + assert captured["drafts"] is True assert captured["host"] == "127.0.0.2" assert captured["port"] == 9999 assert captured["access_log"] is False @@ -132,9 +135,10 @@ def test_rpc_server_cli_uses_mcp_config_server(monkeypatch, tmp_path) -> None: captured["mcp_config_path"] = path return object() - def fake_create_rpc_app(server, *, rpc_path="/rpc"): + def fake_create_rpc_app(server, *, rpc_path="/rpc", drafts=False): captured["server"] = server captured["rpc_path"] = rpc_path + captured["drafts"] = drafts return object() def fake_uvicorn_run(app_obj, *, host, port, access_log): @@ -207,9 +211,10 @@ def test_rpc_server_cli_mcp_config_builds_registry_capable_server( ) captured: dict[str, object] = {} - def fake_create_rpc_app(server, *, rpc_path="/rpc"): + def fake_create_rpc_app(server, *, rpc_path="/rpc", drafts=False): captured["source_registry_admin"] = server.source_registry_admin captured["rpc_path"] = rpc_path + captured["drafts"] = drafts return object() def fake_uvicorn_run(app_obj, *, host, port, access_log): @@ -263,9 +268,10 @@ def test_rpc_server_cli_mcp_config_with_config_uses_transport_settings( ) captured: dict[str, object] = {} - def fake_create_rpc_app(server, *, rpc_path="/rpc"): + def fake_create_rpc_app(server, *, rpc_path="/rpc", drafts=False): captured["server"] = server captured["rpc_path"] = rpc_path + captured["drafts"] = drafts return object() def fake_uvicorn_run(app_obj, *, host, port, access_log): @@ -299,13 +305,15 @@ def test_rpc_server_cli_config_with_mcp_source_uses_mcp_builder( ) -> None: captured = {} - def fake_build_from_workflow_config(config): + def fake_build_from_workflow_config(config, *, drafts=False): captured["source_kinds"] = [source.kind for source in config.server.sources] + captured["build_drafts"] = drafts return object() - def fake_create_rpc_app(server, *, rpc_path="/rpc"): + def fake_create_rpc_app(server, *, rpc_path="/rpc", drafts=False): captured["server"] = server captured["rpc_path"] = rpc_path + captured["drafts"] = drafts return "app" def fake_run(app, *, host, port, access_log): @@ -409,13 +417,15 @@ def test_rpc_server_cli_config_uses_workflow_store_override( ) captured: dict[str, object] = {} - def fake_build_server(config): + def fake_build_server(config, *, drafts=False): captured["workflow_store_root"] = config.server.workflow_store.root + captured["build_drafts"] = drafts return object() - def fake_create_rpc_app(server, *, rpc_path="/rpc"): + def fake_create_rpc_app(server, *, rpc_path="/rpc", drafts=False): captured["server"] = server captured["rpc_path"] = rpc_path + captured["drafts"] = drafts return object() def fake_uvicorn_run(app_obj, *, host, port, access_log): diff --git a/tests/wf_server/test_local_static_server.py b/tests/wf_server/test_local_static_server.py index b3235ea6..559521ac 100644 --- a/tests/wf_server/test_local_static_server.py +++ b/tests/wf_server/test_local_static_server.py @@ -107,9 +107,7 @@ async def test_local_static_server_runs_deployment_and_persists_run(tmp_path) -> assert output["result"] == "hello from server" run_id = run_result["run_id"] assert isinstance(run_id, str) - assert ( - server.stores.run_store.get_run(run_id).id == run_id - ) + assert server.stores.run_store.get_run(run_id).id == run_id async def test_local_static_server_inspects_and_reads_bounded_trace(tmp_path) -> None: @@ -193,6 +191,28 @@ def test_local_static_server_has_no_source_registry_admin(tmp_path) -> None: assert server.source_registry_admin is None +def test_local_static_server_default_composition_has_no_draft_store(tmp_path) -> None: + root = tmp_path / "store" + server = build_local_static_workflow_server(root) + + assert server.stores.draft_workspace_store is None + assert server.context.draft_workspace_store is None + assert server.api.drafts_enabled is False + assert not (root / "draft_workspaces").exists() + + +def test_local_static_server_explicit_draft_composition_has_draft_store( + tmp_path, +) -> None: + root = tmp_path / "store" + server = build_local_static_workflow_server(root, drafts=True) + + assert server.stores.draft_workspace_store is not None + assert server.context.draft_workspace_store is server.stores.draft_workspace_store + assert server.api.drafts_enabled is True + assert (root / "draft_workspaces").is_dir() + + def test_local_static_builtins_are_platform_sources(tmp_path) -> None: server = build_local_static_workflow_server(tmp_path) diff --git a/tests/wf_transport_rpc_http/test_app.py b/tests/wf_transport_rpc_http/test_app.py index 7edb2ebc..9b4c9901 100644 --- a/tests/wf_transport_rpc_http/test_app.py +++ b/tests/wf_transport_rpc_http/test_app.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from typing import Any import httpx @@ -21,6 +22,33 @@ from wf_transport_rpc_http.models import ( ) +@pytest.fixture(autouse=True) +def _draft_enabled_composition(monkeypatch: pytest.MonkeyPatch) -> None: + """Opt draft-focused RPC tests into the otherwise disabled composition.""" + build_local = build_local_static_workflow_server + build_config = build_workflow_server_from_workflow_config + create_app = create_rpc_app + + def draft_local(root, *args, **kwargs): + kwargs.setdefault("drafts", True) + return build_local(root, *args, **kwargs) + + def draft_config(config, *args, **kwargs): + kwargs.setdefault("drafts", True) + return build_config(config, *args, **kwargs) + + def draft_app(server, *args, **kwargs): + kwargs.setdefault("drafts", True) + return create_app(server, *args, **kwargs) + + module = sys.modules[__name__] + monkeypatch.setattr(module, "build_local_static_workflow_server", draft_local) + monkeypatch.setattr( + module, "build_workflow_server_from_workflow_config", draft_config + ) + monkeypatch.setattr(module, "create_rpc_app", draft_app) + + async def _rpc( client: httpx.AsyncClient, method: str, params: dict[str, Any] ) -> dict[str, Any]: diff --git a/tests/wf_transport_rpc_http/test_client.py b/tests/wf_transport_rpc_http/test_client.py index 7df5626b..bd011d99 100644 --- a/tests/wf_transport_rpc_http/test_client.py +++ b/tests/wf_transport_rpc_http/test_client.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from typing import Any import httpx @@ -31,6 +32,25 @@ from wf_transport_rpc_http.client.drafts import RpcDraftClientMixin from wf_transport_rpc_http.client.sources import RpcSourceAdminClientMixin +@pytest.fixture(autouse=True) +def _draft_enabled_composition(monkeypatch: pytest.MonkeyPatch) -> None: + """Opt draft-focused RPC client tests into the explicit draft surface.""" + build_local = build_local_static_workflow_server + create_app = create_rpc_app + + def draft_local(root, *args, **kwargs): + kwargs.setdefault("drafts", True) + return build_local(root, *args, **kwargs) + + def draft_app(server, *args, **kwargs): + kwargs.setdefault("drafts", True) + return create_app(server, *args, **kwargs) + + module = sys.modules[__name__] + monkeypatch.setattr(module, "build_local_static_workflow_server", draft_local) + monkeypatch.setattr(module, "create_rpc_app", draft_app) + + async def test_rpc_client_preserves_structured_jsonrpc_error() -> None: def handler(request: httpx.Request) -> httpx.Response: return httpx.Response( diff --git a/tests/wf_transport_rpc_http/test_openrpc_contract.py b/tests/wf_transport_rpc_http/test_openrpc_contract.py index 4b9669b2..7124b910 100644 --- a/tests/wf_transport_rpc_http/test_openrpc_contract.py +++ b/tests/wf_transport_rpc_http/test_openrpc_contract.py @@ -31,7 +31,12 @@ def _assert_result_component( @pytest.fixture def openrpc_document(tmp_path: Path) -> dict[str, Any]: - app = create_rpc_app(build_local_static_workflow_server(tmp_path / "store")) + # This fixture inventories the complete RPC contract, including the + # explicitly opt-in draft methods. + app = create_rpc_app( + build_local_static_workflow_server(tmp_path / "store", drafts=True), + drafts=True, + ) return app.get_openrpc()