fix: opt draft storage into real callers

This commit is contained in:
lda
2026-08-31 14:07:53 +07:00 Verified
parent ce7e3ed761
commit b377311a6e
21 changed files with 194 additions and 50 deletions
@@ -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 figures absent from the base worktree. No generated `.wf_mcp_store/` or
`test-artifacts/` files are part of this change. `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 ## Fix round 1
- Normal `WorkflowApi`, nested capability/artifact services, durable context, - Normal `WorkflowApi`, nested capability/artifact services, durable context,
+1
View File
@@ -114,6 +114,7 @@ Hypothetically, an application that wants to turn a discovered capability into
a durable run would use the following complete flow: a durable run would use the following complete flow:
```python ```python
from wf_client import App
from wf_authoring import input_from, input_value, output_to, state_path from wf_authoring import input_from, input_value, output_to, state_path
app = App.from_http_jsonrpc("http://localhost:8765/rpc") app = App.from_http_jsonrpc("http://localhost:8765/rpc")
+12 -3
View File
@@ -22,12 +22,21 @@ class WorkflowStores:
run_store: RunStore run_store: RunStore
def file_workflow_stores(root: str | Path) -> WorkflowStores: def file_workflow_stores(
"""Create process-local file-backed workflow stores under one root.""" 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) store_root = Path(root)
return WorkflowStores( return WorkflowStores(
artifact_store=FileWorkflowArtifactStore(store_root), 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), run_store=FileRunStore(store_root),
) )
+5 -2
View File
@@ -97,7 +97,9 @@ def build_workflow_server_from_workflow_config(
"""Build the local server without importing the server runtime at CLI startup.""" """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 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( def load_cli_context(
@@ -150,7 +152,8 @@ def load_cli_context(
return CliContext( return CliContext(
config_path=resolved_config_path, config_path=resolved_config_path,
service=service, 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)), source_admin=WorkflowSourceAdminApi(context_from_service(service)),
admin=WorkflowAdminApi( admin=WorkflowAdminApi(
connections=service.connection_service, connections=service.connection_service,
+3 -1
View File
@@ -163,7 +163,9 @@ def build_service_from_config(config: BrokerConfig) -> WfMcpService:
"""Create a broker service with SDK adapters for configured connections.""" """Create a broker service with SDK adapters for configured connections."""
runtime_factory = PersistentSessionFactory() runtime_factory = PersistentSessionFactory()
store_roots = config.store_roots 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 # Keep FileStore as the compatibility facade on WfMcpService.store while
# focused services receive role-specific stores. # focused services receive role-specific stores.
auth_store = FileAuthStore(store_roots.auth_root) auth_store = FileAuthStore(store_roots.auth_root)
+2 -1
View File
@@ -64,7 +64,8 @@ def workflow_server_from_service(
raise ValueError("MCP-backed WorkflowServer requires workflow stores") raise ValueError("MCP-backed WorkflowServer requires workflow stores")
context = context_from_service(service) 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( source_diagnostics = SourceDiagnosticsProvider(
connection_lookup=service.connections.get, connection_lookup=service.connections.get,
auth_store=service.auth_store or service.store, auth_store=service.auth_store or service.store,
+4 -2
View File
@@ -13,14 +13,16 @@ if TYPE_CHECKING:
class WorkflowSurfaceHandlers(WorkflowApi): class WorkflowSurfaceHandlers(WorkflowApi):
"""Compatibility wrapper for old wf_mcp.workflow_surface imports. """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 directly. This shim keeps tests and legacy broker artifact tools working
for legacy callers. for legacy callers.
""" """
def __init__(self, service: WfMcpService) -> None: def __init__(self, service: WfMcpService) -> None:
self.service = service 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"] __all__ = ["WorkflowSurfaceHandlers"]
+3 -1
View File
@@ -71,7 +71,9 @@ from .models import (
def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None: def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None:
"""Register stable workflow tools on the public MCP server surface.""" """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( @server.tool(
name="wf.workflow.list_artifacts", name="wf.workflow.list_artifacts",
+9 -3
View File
@@ -68,7 +68,11 @@ def serve(
workflow_config = load_workflow_config(config) workflow_config = load_workflow_config(config)
store = workflow_config.server.workflow_store store = workflow_config.server.workflow_store
if server is None and store_root is None: 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: elif server is None:
has_mcp_sources = any( has_mcp_sources = any(
getattr(source, "kind", None) == "mcp" getattr(source, "kind", None) == "mcp"
@@ -101,9 +105,11 @@ def serve(
raise typer.BadParameter( raise typer.BadParameter(
"--store-root is required when --config is not supplied" "--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( uvicorn.run(
rpc_app, rpc_app,
host=resolved_host or "127.0.0.1", host=resolved_host or "127.0.0.1",
+6 -1
View File
@@ -53,11 +53,15 @@ def _build_mcp_workflow_server_from_legacy_config(path: Path) -> WorkflowServer:
def build_workflow_server_from_workflow_config( def build_workflow_server_from_workflow_config(
config: WorkflowConfigFile, config: WorkflowConfigFile,
*,
drafts: bool = False,
) -> WorkflowServer: ) -> WorkflowServer:
"""Build a WorkflowServer from neutral workflow config. """Build a WorkflowServer from neutral workflow config.
Local/static configs use built-in sources. Configs with ``kind: "mcp"`` 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): if _has_mcp_sources(config):
return _build_mcp_workflow_server_from_workflow_config(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") raise ValueError("wf-rpc-server currently requires filesystem store")
return build_local_static_workflow_server( return build_local_static_workflow_server(
store.root, store.root,
drafts=drafts,
extra_sources=collect_static_sources(_static_source_providers(config)), extra_sources=collect_static_sources(_static_source_providers(config)),
) )
+1 -1
View File
@@ -299,7 +299,7 @@ def build_local_static_workflow_server(
) -> WorkflowServer: ) -> WorkflowServer:
"""Build a durable local/static server, with drafts as an explicit opt-in.""" """Build a durable local/static server, with drafts as an explicit opt-in."""
config = WorkflowServerConfig(store_root=Path(root)) config = WorkflowServerConfig(store_root=Path(root))
stores = file_workflow_stores(config.store_root) stores = file_workflow_stores(config.store_root, drafts=drafts)
events = InMemoryWorkflowEventRecorder() events = InMemoryWorkflowEventRecorder()
sources = builtin_sources() sources = builtin_sources()
if extra_sources: if extra_sources:
+2 -2
View File
@@ -131,7 +131,7 @@ def _artifact_api(
) )
service.register_specs("demo.personal", echo_tool) service.register_specs("demo.personal", echo_tool)
context = context_from_service(service) context = context_from_service(service)
return WorkflowArtifactApi(context), service return WorkflowArtifactApi(context, drafts=True), service
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -454,7 +454,7 @@ def _api(root: Path) -> WorkflowApi:
artifact_store=FileWorkflowArtifactStore(root), artifact_store=FileWorkflowArtifactStore(root),
draft_workspace_store=FileDraftWorkspaceStore(mcp_root), draft_workspace_store=FileDraftWorkspaceStore(mcp_root),
) )
return WorkflowApi(context_from_service(service)) return WorkflowApi(context_from_service(service), drafts=True)
@pytest.mark.asyncio @pytest.mark.asyncio
+15 -3
View File
@@ -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" root = tmp_path / "wf_api_file_workflow_stores"
stores = file_workflow_stores(root) stores = file_workflow_stores(root)
assert isinstance(stores, WorkflowStores) assert isinstance(stores, WorkflowStores)
assert isinstance(stores.artifact_store, FileWorkflowArtifactStore) 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 isinstance(stores.run_store, FileRunStore)
assert stores.artifact_store.root == root assert stores.artifact_store.root == root
assert stores.draft_workspace_store.root == root
assert stores.run_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: def test_wf_api_exports_workflow_stores() -> None:
+6 -14
View File
@@ -16,7 +16,6 @@ from wf_cli.context import (
rpc_timeout_from_context, rpc_timeout_from_context,
rpc_url_from_context, rpc_url_from_context,
) )
from wf_server.config import build_workflow_server_from_workflow_config
from .conftest import write_python_source_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( def test_load_cli_context_local_uses_workflow_store_override(
tmp_path: Path, tmp_path: Path,
monkeypatch,
) -> None: ) -> None:
config_path = tmp_path / "wf.json" config_path = tmp_path / "wf.json"
config_path.write_text( config_path.write_text(
@@ -125,21 +123,15 @@ def test_load_cli_context_local_uses_workflow_store_override(
), ),
encoding="utf-8", 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) context = load_cli_context(config_path)
assert context.service is None 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 @pytest.mark.asyncio
+3 -3
View File
@@ -101,7 +101,7 @@ async def test_registered_output_bindings_tool_delegates_typed_bindings_once(
recorder = RecordingWorkflowHandler() recorder = RecordingWorkflowHandler()
monkeypatch.setattr( monkeypatch.setattr(
"wf_mcp.workflow_surface.tools.WorkflowApi", "wf_mcp.workflow_surface.tools.WorkflowApi",
lambda _context: recorder, lambda _context, **_kwargs: recorder,
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(tmp_path / "tool_invocation_store"), store=FileStore(tmp_path / "tool_invocation_store"),
@@ -167,7 +167,7 @@ async def test_registered_workflow_output_bindings_tool_preserves_union_order(
recorder = RecordingWorkflowHandler() recorder = RecordingWorkflowHandler()
monkeypatch.setattr( monkeypatch.setattr(
"wf_mcp.workflow_surface.tools.WorkflowApi", "wf_mcp.workflow_surface.tools.WorkflowApi",
lambda _context: recorder, lambda _context, **_kwargs: recorder,
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(tmp_path / "workflow_output_tool_store"), store=FileStore(tmp_path / "workflow_output_tool_store"),
@@ -242,7 +242,7 @@ async def test_registered_capability_tools_delegate_presence_aware_requests(
recorder = RecordingWorkflowHandler() recorder = RecordingWorkflowHandler()
monkeypatch.setattr( monkeypatch.setattr(
"wf_mcp.workflow_surface.tools.WorkflowApi", "wf_mcp.workflow_surface.tools.WorkflowApi",
lambda _context: recorder, lambda _context, **_kwargs: recorder,
) )
service = WfMcpService( service = WfMcpService(
store=FileStore(tmp_path / "capability_tool_store"), store=FileStore(tmp_path / "capability_tool_store"),
+2
View File
@@ -69,6 +69,8 @@ def test_workflow_server_from_service_wires_neutral_surfaces(tmp_path) -> None:
assert isinstance(server, WorkflowServer) assert isinstance(server, WorkflowServer)
assert server.config.store_root == config.store_root assert server.config.store_root == config.store_root
assert server.api.context is server.context 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.source_registry_admin is not None
assert server.admin.connections is service.connection_service assert server.admin.connections is service.connection_service
assert server.admin.events is service.events assert server.admin.events is service.events
+19 -9
View File
@@ -83,13 +83,15 @@ def test_rpc_server_cli_uses_configured_store_and_transport(
) )
captured: dict[str, object] = {} captured: dict[str, object] = {}
def fake_build_server(config): def fake_build_server(config, *, drafts=False):
captured["store_root"] = config.server.store.root captured["store_root"] = config.server.store.root
captured["drafts"] = drafts
return object() 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["server"] = server
captured["rpc_path"] = rpc_path captured["rpc_path"] = rpc_path
captured["drafts"] = drafts
return object() return object()
def fake_uvicorn_run(app_obj, *, host, port, access_log): 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 result.exit_code == 0, result.output
assert captured["store_root"] == (tmp_path / ".wf_store").resolve() assert captured["store_root"] == (tmp_path / ".wf_store").resolve()
assert captured["rpc_path"] == "/workflow-rpc" assert captured["rpc_path"] == "/workflow-rpc"
assert captured["drafts"] is True
assert captured["host"] == "127.0.0.2" assert captured["host"] == "127.0.0.2"
assert captured["port"] == 9999 assert captured["port"] == 9999
assert captured["access_log"] is False 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 captured["mcp_config_path"] = path
return object() 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["server"] = server
captured["rpc_path"] = rpc_path captured["rpc_path"] = rpc_path
captured["drafts"] = drafts
return object() return object()
def fake_uvicorn_run(app_obj, *, host, port, access_log): 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] = {} 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["source_registry_admin"] = server.source_registry_admin
captured["rpc_path"] = rpc_path captured["rpc_path"] = rpc_path
captured["drafts"] = drafts
return object() return object()
def fake_uvicorn_run(app_obj, *, host, port, access_log): 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] = {} 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["server"] = server
captured["rpc_path"] = rpc_path captured["rpc_path"] = rpc_path
captured["drafts"] = drafts
return object() return object()
def fake_uvicorn_run(app_obj, *, host, port, access_log): 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: ) -> None:
captured = {} 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["source_kinds"] = [source.kind for source in config.server.sources]
captured["build_drafts"] = drafts
return object() 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["server"] = server
captured["rpc_path"] = rpc_path captured["rpc_path"] = rpc_path
captured["drafts"] = drafts
return "app" return "app"
def fake_run(app, *, host, port, access_log): 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] = {} 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["workflow_store_root"] = config.server.workflow_store.root
captured["build_drafts"] = drafts
return object() 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["server"] = server
captured["rpc_path"] = rpc_path captured["rpc_path"] = rpc_path
captured["drafts"] = drafts
return object() return object()
def fake_uvicorn_run(app_obj, *, host, port, access_log): def fake_uvicorn_run(app_obj, *, host, port, access_log):
+23 -3
View File
@@ -107,9 +107,7 @@ async def test_local_static_server_runs_deployment_and_persists_run(tmp_path) ->
assert output["result"] == "hello from server" assert output["result"] == "hello from server"
run_id = run_result["run_id"] run_id = run_result["run_id"]
assert isinstance(run_id, str) assert isinstance(run_id, str)
assert ( assert server.stores.run_store.get_run(run_id).id == run_id
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: 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 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: def test_local_static_builtins_are_platform_sources(tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path) server = build_local_static_workflow_server(tmp_path)
+28
View File
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import sys
from typing import Any from typing import Any
import httpx 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( async def _rpc(
client: httpx.AsyncClient, method: str, params: dict[str, Any] client: httpx.AsyncClient, method: str, params: dict[str, Any]
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import sys
from typing import Any from typing import Any
import httpx import httpx
@@ -31,6 +32,25 @@ from wf_transport_rpc_http.client.drafts import RpcDraftClientMixin
from wf_transport_rpc_http.client.sources import RpcSourceAdminClientMixin 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: async def test_rpc_client_preserves_structured_jsonrpc_error() -> None:
def handler(request: httpx.Request) -> httpx.Response: def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response( return httpx.Response(
@@ -31,7 +31,12 @@ def _assert_result_component(
@pytest.fixture @pytest.fixture
def openrpc_document(tmp_path: Path) -> dict[str, Any]: 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() return app.get_openrpc()