fix: harden Python workflow client boundary
This commit is contained in:
@@ -256,9 +256,7 @@ async def test_lda_report_workflow_artifact_interrupt_resume_path(
|
||||
"approved",
|
||||
"selected_issue_ids",
|
||||
}
|
||||
proposed_ids = [
|
||||
issue["id"] for issue in interrupt["payload"]["proposed_issues"]
|
||||
]
|
||||
proposed_ids = [issue["id"] for issue in interrupt["payload"]["proposed_issues"]]
|
||||
assert proposed_ids
|
||||
started_run_id = started["run_id"]
|
||||
assert isinstance(started_run_id, str)
|
||||
|
||||
@@ -39,7 +39,7 @@ def _capability_api(
|
||||
)
|
||||
service.register_specs("demo.personal", failing_tool)
|
||||
context = context_from_service(service)
|
||||
return WorkflowCapabilityApi(context), service
|
||||
return WorkflowCapabilityApi(context, drafts=True), service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -226,6 +226,7 @@ async def test_create_draft_workspace_from_capability(tmp_path: Path) -> None:
|
||||
assert "next_actions" in result
|
||||
assert result["wrapper_hints"]["capability_name"] == "demo.personal.echo_tool"
|
||||
|
||||
assert api.drafts is not None
|
||||
fetched = await api.drafts.get_draft_workspace(
|
||||
workspace_id="echo_ws", include_draft=True
|
||||
)
|
||||
|
||||
@@ -42,12 +42,13 @@ def _composite_concat_draft() -> dict[str, object]:
|
||||
async def test_composite_concat_runs_through_the_platform_registry(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
await server.api.create_draft_workspace(
|
||||
workspace_id="composite_concat",
|
||||
draft=_composite_concat_draft(),
|
||||
)
|
||||
|
||||
assert server.api.draft_authoring is not None
|
||||
authored = await server.api.draft_authoring.set_step_input_bindings(
|
||||
workspace_id="composite_concat",
|
||||
revision=1,
|
||||
|
||||
@@ -171,7 +171,7 @@ async def test_inspect_draft_authoring_contract_projects_selected_capability(
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
}
|
||||
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
inventory = _authoring_inventory(
|
||||
await api.inspect_draft_authoring_contract(
|
||||
@@ -219,7 +219,7 @@ async def test_inspect_draft_authoring_contract_tolerates_invalid_workflow_schem
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
}
|
||||
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
inventory = _authoring_inventory(
|
||||
await api.inspect_draft_authoring_contract(
|
||||
@@ -256,7 +256,7 @@ async def test_inspect_draft_authoring_contract_resolves_saved_wrapper_capabilit
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
}
|
||||
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
inventory = _authoring_inventory(
|
||||
await api.inspect_draft_authoring_contract(
|
||||
@@ -296,7 +296,7 @@ async def test_inspect_draft_authoring_contract_preserves_empty_capability_schem
|
||||
"properties": {"echoed": {"type": "string"}},
|
||||
}
|
||||
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
inventory = _authoring_inventory(
|
||||
await api.inspect_draft_authoring_contract(
|
||||
@@ -344,7 +344,7 @@ async def test_inspect_draft_authoring_contract_warns_for_invalid_capability_sch
|
||||
workspace_id="authoring",
|
||||
draft=_echo_draft(),
|
||||
)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
inventory = _authoring_inventory(
|
||||
await api.inspect_draft_authoring_contract(
|
||||
@@ -375,7 +375,7 @@ async def test_inspect_draft_authoring_contract_rejects_unknown_selected_step(
|
||||
await draft_api.create_draft_workspace(
|
||||
workspace_id="authoring", draft=_echo_draft()
|
||||
)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
with pytest.raises(KeyError, match="unknown draft step"):
|
||||
await api.inspect_draft_authoring_contract(
|
||||
@@ -397,7 +397,7 @@ async def test_inspect_draft_authoring_contract_tolerates_invalid_selected_step(
|
||||
draft["steps"] = {"broken": {"unknown_kind": {}}}
|
||||
draft["start"] = "broken"
|
||||
await draft_api.create_draft_workspace(workspace_id="authoring", draft=draft)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
inventory = _authoring_inventory(
|
||||
await api.inspect_draft_authoring_contract(
|
||||
@@ -426,7 +426,7 @@ async def test_inspect_draft_authoring_contract_stale_revision_is_read_only(
|
||||
await draft_api.create_draft_workspace(
|
||||
workspace_id="authoring", draft=_echo_draft()
|
||||
)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
changed = await api.set_draft_name(
|
||||
workspace_id="authoring",
|
||||
revision=1,
|
||||
@@ -1023,7 +1023,7 @@ async def _create_structured_binding_api(
|
||||
workspace_id=workspace_id,
|
||||
draft=_structured_report_draft(),
|
||||
)
|
||||
return draft_api, service, WorkflowApi(authoring.context)
|
||||
return draft_api, service, WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
|
||||
async def _create_nested_output_binding_api(
|
||||
@@ -1058,7 +1058,7 @@ async def _create_nested_output_binding_api(
|
||||
workspace_id=workspace_id,
|
||||
draft=_nested_report_draft(),
|
||||
)
|
||||
return draft_api, service, WorkflowApi(authoring.context)
|
||||
return draft_api, service, WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1163,7 +1163,7 @@ async def test_create_empty_draft_workspace_persists_invalid_skeleton(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_empty")
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
created = await facade.create_empty_draft_workspace(
|
||||
workspace_id="control_first",
|
||||
@@ -1198,7 +1198,7 @@ async def test_create_empty_draft_workspace_preserves_custom_contract(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_contract")
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {"topic": {"type": "string"}},
|
||||
@@ -1249,7 +1249,7 @@ async def test_create_empty_draft_workspace_isolates_default_schemas(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_schema_isolation")
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {"topic": {"type": "string"}},
|
||||
@@ -1283,7 +1283,7 @@ async def test_create_empty_draft_workspace_reports_duplicate_conflict(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_conflict")
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
await facade.create_empty_draft_workspace(
|
||||
workspace_id="control_first",
|
||||
name="control_first",
|
||||
@@ -1317,7 +1317,7 @@ async def test_create_empty_draft_workspace_rejects_invalid_contract_before_muta
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_create_rejected")
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await facade.create_empty_draft_workspace(
|
||||
@@ -1335,7 +1335,7 @@ async def test_set_draft_start_and_contract_replace_top_level_fields_atomically(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "drafts_set_lifecycle")
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
await facade.create_empty_draft_workspace(
|
||||
workspace_id="control_first",
|
||||
name="control_first",
|
||||
@@ -1412,7 +1412,7 @@ async def test_lifecycle_edits_reject_invalid_envelopes_without_mutation(
|
||||
tmp_path / f"drafts_lifecycle_rejected_{operation}"
|
||||
)
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
await facade.create_empty_draft_workspace(
|
||||
workspace_id="control_first",
|
||||
name="control_first",
|
||||
@@ -1453,7 +1453,7 @@ async def test_lifecycle_edits_report_stale_revision_without_mutation(
|
||||
tmp_path / f"drafts_lifecycle_stale_{operation}"
|
||||
)
|
||||
_drafts, _service, authoring = _draft_api(artifact_store)
|
||||
facade = WorkflowApi(authoring.context)
|
||||
facade = WorkflowApi(authoring.context, drafts=True)
|
||||
await facade.create_empty_draft_workspace(
|
||||
workspace_id="control_first",
|
||||
name="control_first",
|
||||
@@ -2187,7 +2187,7 @@ async def test_facade_delegates_semantic_authoring_to_authoring_service(
|
||||
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
||||
|
||||
context = context_from_service(service)
|
||||
facade = WorkflowApi(context)
|
||||
facade = WorkflowApi(context, drafts=True)
|
||||
|
||||
assert facade.draft_authoring is not None
|
||||
assert isinstance(facade.draft_authoring, WorkflowDraftAuthoringApi)
|
||||
@@ -3469,7 +3469,7 @@ async def test_set_step_input_bindings_rejects_remote_target_reference_without_m
|
||||
workspace_id="remote_target",
|
||||
draft=_structured_report_draft("demo.personal.remote_structured_report"),
|
||||
)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
before = await draft_api.get_draft_workspace(
|
||||
workspace_id="remote_target",
|
||||
include_draft=True,
|
||||
@@ -3506,7 +3506,7 @@ async def test_set_step_input_bindings_rejects_non_capability_step_without_mutat
|
||||
draft = _structured_report_draft()
|
||||
draft["steps"]["report"] = {"join": {}}
|
||||
await draft_api.create_draft_workspace(workspace_id="non_capability", draft=draft)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
before = await draft_api.get_draft_workspace(
|
||||
workspace_id="non_capability",
|
||||
include_draft=True,
|
||||
@@ -4007,7 +4007,7 @@ async def test_add_step_accepts_every_typed_draft_step(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / f"draft_add_{step_name}")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
|
||||
step = TypeAdapter(DraftStep).validate_python(step_payload)
|
||||
@@ -4033,7 +4033,7 @@ async def test_add_step_routes_incoming_and_outgoing_edges_atomically(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_routes")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
|
||||
step = TypeAdapter(DraftStep).validate_python(
|
||||
@@ -4062,7 +4062,7 @@ async def test_add_step_stale_revision_wins_over_content_preflight(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_stale")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
before = await draft_api.get_draft_workspace(
|
||||
workspace_id="draft_ws", include_draft=True
|
||||
@@ -4135,7 +4135,7 @@ async def test_add_step_adds_missing_incoming_route_parent_atomically(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_missing_parent")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
draft = _echo_draft()
|
||||
draft["routes"] = {}
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=draft)
|
||||
@@ -4164,7 +4164,7 @@ async def test_add_step_distinguishes_missing_and_explicit_empty_routes(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_empty_routes")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
|
||||
step_adapter = TypeAdapter(DraftStep)
|
||||
@@ -4207,7 +4207,7 @@ async def test_add_step_rejects_unknown_incoming_outcome_without_mutation(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_bad_incoming")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
|
||||
draft_store = authoring.drafts._draft_store()
|
||||
@@ -4277,7 +4277,7 @@ async def test_add_step_rejects_invalid_routing_inputs_atomically(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_errors")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
|
||||
await _assert_add_step_rejected_without_mutation(
|
||||
@@ -4338,7 +4338,7 @@ async def test_add_step_rejects_routes_for_non_routable_steps_atomically(
|
||||
) -> None:
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_forbidden_routes")
|
||||
draft_api, _service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
|
||||
before = await draft_api.get_draft_workspace(
|
||||
@@ -4367,7 +4367,7 @@ async def test_add_step_accepts_incomplete_declared_route_subset(
|
||||
artifact_store = FileWorkflowArtifactStore(tmp_path / "draft_add_partial_routes")
|
||||
draft_api, service, authoring = _draft_api(artifact_store, register_echo=True)
|
||||
service.register_specs("demo.personal", echo_tool, _snapshot_tool)
|
||||
api = WorkflowApi(authoring.context)
|
||||
api = WorkflowApi(authoring.context, drafts=True)
|
||||
await draft_api.create_draft_workspace(workspace_id="draft_ws", draft=_echo_draft())
|
||||
|
||||
step = TypeAdapter(DraftStep).validate_python(
|
||||
@@ -6300,7 +6300,7 @@ def _browser_click_api(
|
||||
_collect_snapshots,
|
||||
)
|
||||
context = context_from_service(service)
|
||||
return WorkflowApi(context), service
|
||||
return WorkflowApi(context, drafts=True), service
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -8,6 +8,7 @@ import typer
|
||||
from typer.core import TyperCommand
|
||||
|
||||
from wf_api import WorkflowApi
|
||||
from wf_artifacts import FileWorkflowArtifactStore
|
||||
from wf_cli.context import (
|
||||
CliTyperState,
|
||||
config_path_from_context,
|
||||
@@ -128,10 +129,9 @@ def test_load_cli_context_local_uses_workflow_store_override(
|
||||
assert context.service is None
|
||||
assert isinstance(context.handlers, WorkflowApi)
|
||||
assert context.handlers.drafts_enabled is True
|
||||
assert (
|
||||
context.handlers.context.artifact_store.root
|
||||
== (tmp_path / ".workflow").resolve()
|
||||
)
|
||||
artifact_store = context.handlers.context.artifact_store
|
||||
assert isinstance(artifact_store, FileWorkflowArtifactStore)
|
||||
assert artifact_store.root == (tmp_path / ".workflow").resolve()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -329,7 +329,7 @@ def _patch_rpc_client_to_server(monkeypatch, server) -> None:
|
||||
url=url,
|
||||
timeout_seconds=timeout_seconds,
|
||||
http_client=httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server)),
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server, drafts=True)),
|
||||
base_url="http://test",
|
||||
),
|
||||
)
|
||||
@@ -340,7 +340,7 @@ def _patch_rpc_client_to_server(monkeypatch, server) -> None:
|
||||
|
||||
|
||||
def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -433,7 +433,7 @@ def test_wf_cap_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_source_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -511,7 +511,7 @@ def test_wf_remote_source_inspect_formats_expected_rpc_error(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -607,7 +607,7 @@ def test_wf_verbose_shows_full_traceback_for_unexpected_error(
|
||||
|
||||
|
||||
def test_wf_admin_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
server.events.record_workflow_event(
|
||||
"workflow_test_event",
|
||||
capability_id="workflow.demo.v1",
|
||||
@@ -632,7 +632,7 @@ def test_wf_admin_commands_use_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_remote_draft_artifact_deploy_lifecycle(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -760,7 +760,7 @@ def test_wf_remote_draft_artifact_deploy_lifecycle(monkeypatch, tmp_path) -> Non
|
||||
|
||||
|
||||
def test_wf_remote_capability_free_draft_lifecycle(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -833,7 +833,7 @@ def test_wf_remote_capability_free_draft_lifecycle(monkeypatch, tmp_path) -> Non
|
||||
def test_wf_draft_export_uses_remote_get_and_writes_only_draft(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_calls: list[tuple[str, dict[str, Any]]] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -885,7 +885,7 @@ def test_wf_draft_export_uses_remote_get_and_writes_only_draft(
|
||||
def test_wf_draft_import_uses_exact_remote_replacement_payload(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
asyncio.run(
|
||||
server.api.create_empty_draft_workspace(
|
||||
workspace_id="source_ws",
|
||||
@@ -956,7 +956,7 @@ def test_wf_draft_import_uses_exact_remote_replacement_payload(
|
||||
def test_wf_draft_transfer_round_trip_preserves_document_and_destination_id(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
asyncio.run(
|
||||
server.api.create_empty_draft_workspace(
|
||||
workspace_id="source_ws",
|
||||
@@ -1023,7 +1023,7 @@ def test_wf_draft_transfer_round_trip_preserves_document_and_destination_id(
|
||||
|
||||
|
||||
def test_wf_remote_run_resume_interrupted_deployment(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
asyncio.run(
|
||||
server.api.create_artifact_from_plan(
|
||||
artifact_id="remote_approval",
|
||||
@@ -1083,7 +1083,7 @@ def test_wf_remote_run_resume_interrupted_deployment(monkeypatch, tmp_path) -> N
|
||||
|
||||
|
||||
def test_wf_status_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
asyncio.run(
|
||||
server.api.create_artifact_from_plan(
|
||||
artifact_id="status_constant",
|
||||
@@ -1143,7 +1143,7 @@ def test_wf_status_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_status_reports_rpc_config_target(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text(
|
||||
@@ -1178,7 +1178,7 @@ def test_wf_status_reports_rpc_config_target(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_draft_delete_requires_confirm(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -1192,7 +1192,7 @@ def test_wf_draft_delete_requires_confirm(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_draft_delete_succeeds_with_confirm(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -1225,7 +1225,7 @@ def test_wf_draft_delete_succeeds_with_confirm(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_source_diagnose_uses_rpc_url_override(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -1300,7 +1300,7 @@ def test_wf_draft_create_reports_optional_inputs_without_binding(
|
||||
def test_wf_draft_set_input_bindings_preserves_composite_expression_over_rpc(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_calls: list[tuple[str, dict[str, Any]]] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -1381,7 +1381,7 @@ def test_wf_draft_set_input_bindings_preserves_composite_expression_over_rpc(
|
||||
|
||||
|
||||
def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -1536,7 +1536,7 @@ def test_wf_draft_focused_edit_commands_use_rpc_target(monkeypatch, tmp_path) ->
|
||||
def test_wf_draft_set_workflow_output_replaces_canonical_bindings_over_rpc(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_calls: list[tuple[str, dict[str, Any]]] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -1625,7 +1625,7 @@ def test_wf_draft_set_workflow_output_replaces_canonical_bindings_over_rpc(
|
||||
def test_wf_draft_set_workflow_output_merge_uses_compatibility_rpc_target(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_methods: list[str] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -1685,7 +1685,7 @@ def test_wf_draft_set_workflow_output_merge_uses_compatibility_rpc_target(
|
||||
def test_wf_draft_set_workflow_output_merge_reports_canonical_replacement(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_methods: list[str] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -1760,7 +1760,7 @@ def test_wf_draft_set_workflow_output_merge_reports_canonical_replacement(
|
||||
|
||||
|
||||
def test_wf_draft_remove_route_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -1811,7 +1811,7 @@ def test_wf_draft_remove_route_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_draft_bind_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -1868,7 +1868,7 @@ def test_wf_draft_bind_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
def test_wf_draft_set_input_preserves_nested_target_over_rpc(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -1921,7 +1921,7 @@ def test_wf_draft_set_input_preserves_nested_target_over_rpc(
|
||||
def test_wf_draft_set_input_replaces_canonical_bindings_over_rpc(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_methods: list[str] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -2024,7 +2024,7 @@ def test_wf_draft_set_input_replaces_canonical_bindings_over_rpc(
|
||||
def test_wf_draft_set_output_replaces_canonical_bindings_over_rpc(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_calls: list[tuple[str, dict[str, Any]]] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -2117,7 +2117,7 @@ def test_wf_draft_set_output_replaces_canonical_bindings_over_rpc(
|
||||
|
||||
|
||||
def test_wf_draft_add_capability_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_methods: list[str] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -2187,7 +2187,7 @@ def test_wf_draft_add_capability_uses_rpc_target(monkeypatch, tmp_path) -> None:
|
||||
def test_wf_draft_capability_add_and_update_preserve_rpc_payloads(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_calls: list[tuple[str, dict[str, Any]]] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -2289,7 +2289,7 @@ def test_wf_draft_capability_add_and_update_preserve_rpc_payloads(
|
||||
def test_wf_draft_add_control_steps_use_generic_rpc_target(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
rpc_methods: list[str] = []
|
||||
original_call = RpcClientTransport._call
|
||||
@@ -2544,7 +2544,7 @@ def test_wf_draft_add_control_steps_use_generic_rpc_target(
|
||||
def test_wf_draft_add_capability_reports_bare_output_target_without_traceback(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -2592,7 +2592,7 @@ def test_wf_draft_add_capability_reports_bare_output_target_without_traceback(
|
||||
|
||||
|
||||
def test_wf_draft_compile_prints_compiled_plan(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
@@ -2625,7 +2625,7 @@ def test_wf_draft_compile_prints_compiled_plan(monkeypatch, tmp_path) -> None:
|
||||
def test_wf_draft_compile_invalid_prints_diagnostics_to_stderr(
|
||||
monkeypatch, tmp_path
|
||||
) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
asyncio.run(
|
||||
server.api.create_draft_workspace(
|
||||
workspace_id="invalid_compile_ws",
|
||||
@@ -2672,7 +2672,7 @@ def test_wf_draft_compile_invalid_prints_diagnostics_to_stderr(
|
||||
|
||||
|
||||
def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
asyncio.run(
|
||||
server.api.create_artifact_from_plan(
|
||||
artifact_id="alias_artifact",
|
||||
@@ -2710,7 +2710,7 @@ def test_wf_deploy_create_alias_saves_deployment(monkeypatch, tmp_path) -> None:
|
||||
|
||||
|
||||
def test_wf_draft_forward_route_invalid_via_rpc(monkeypatch, tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
_patch_rpc_client_to_server(monkeypatch, server)
|
||||
config_path = tmp_path / "wf.json"
|
||||
config_path.write_text('{"version": 1}', encoding="utf-8")
|
||||
|
||||
+154
-2
@@ -5,8 +5,14 @@ from typing import Any, cast
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from wf_client import App, CapabilitySummary, Page
|
||||
from wf_client.errors import InvalidResponse
|
||||
import wf_client
|
||||
from wf_client import App, CapabilitySummary, Page, WorkflowClientError
|
||||
from wf_client.errors import (
|
||||
CapabilityNotFound,
|
||||
InvalidResponse,
|
||||
ProtocolError,
|
||||
TransportError,
|
||||
)
|
||||
from wf_client.protocols import WorkflowClientPort
|
||||
from wf_platform import CapabilityRef
|
||||
|
||||
@@ -92,6 +98,152 @@ def test_from_http_jsonrpc_is_lazy(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_package_does_not_export_internal_port_or_codecs() -> None:
|
||||
assert not hasattr(wf_client, "WorkflowClientPort")
|
||||
assert not hasattr(wf_client, "DecodedRunResult")
|
||||
assert not hasattr(wf_client, "decode_run_result")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_app_translates_connection_failure_to_public_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def fail_post(*args: object, **kwargs: object) -> httpx.Response:
|
||||
raise httpx.ConnectError("connection refused")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "post", fail_post)
|
||||
app = App.from_http_jsonrpc("http://unreachable.test/rpc")
|
||||
|
||||
with pytest.raises(WorkflowClientError) as raised:
|
||||
await app.capability("app.default.search")
|
||||
|
||||
assert isinstance(raised.value, TransportError)
|
||||
assert "connection refused" in str(raised.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("failure", ["http", "json", "json-array"])
|
||||
async def test_http_app_translates_http_and_json_failures(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
failure: str,
|
||||
) -> None:
|
||||
async def fail_post(*args: object, **kwargs: object) -> httpx.Response:
|
||||
request = httpx.Request("POST", "http://test/rpc")
|
||||
if failure == "http":
|
||||
return httpx.Response(503, request=request)
|
||||
if failure == "json-array":
|
||||
return httpx.Response(200, request=request, json=[])
|
||||
return httpx.Response(200, request=request, content=b"not-json")
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "post", fail_post)
|
||||
app = App.from_http_jsonrpc("http://test/rpc")
|
||||
|
||||
with pytest.raises(WorkflowClientError) as raised:
|
||||
await app.capability("app.default.search")
|
||||
|
||||
expected_type = ProtocolError if failure == "json-array" else TransportError
|
||||
assert isinstance(raised.value, expected_type)
|
||||
assert "workflow.capabilities.inspect" in str(raised.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_app_translates_known_workflow_protocol_error(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def error_post(*args: object, **kwargs: object) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
request=httpx.Request("POST", "http://test/rpc"),
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": "request",
|
||||
"error": {
|
||||
"code": 5000,
|
||||
"message": "Workflow operation failed",
|
||||
"data": {
|
||||
"code": "capability_not_found",
|
||||
"message": "unknown capability app.default.search",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "post", error_post)
|
||||
app = App.from_http_jsonrpc("http://test/rpc")
|
||||
|
||||
with pytest.raises(WorkflowClientError) as raised:
|
||||
await app.capability("app.default.search")
|
||||
|
||||
assert isinstance(raised.value, CapabilityNotFound)
|
||||
assert "unknown capability" in str(raised.value)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_app_preserves_unknown_protocol_error_details(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
data = {"code": "future_workflow_error", "message": "future detail", "retry": 3}
|
||||
|
||||
async def error_post(*args: object, **kwargs: object) -> httpx.Response:
|
||||
return httpx.Response(
|
||||
200,
|
||||
request=httpx.Request("POST", "http://test/rpc"),
|
||||
json={
|
||||
"jsonrpc": "2.0",
|
||||
"id": "request",
|
||||
"error": {
|
||||
"code": 5999,
|
||||
"message": "Future workflow error",
|
||||
"data": data,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(httpx.AsyncClient, "post", error_post)
|
||||
app = App.from_http_jsonrpc("http://test/rpc")
|
||||
|
||||
with pytest.raises(ProtocolError) as raised:
|
||||
await app.capability("app.default.search")
|
||||
|
||||
assert raised.value.code == 5999
|
||||
assert raised.value.message == "Future workflow error"
|
||||
assert raised.value.data == data
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_workflow_rejects_mismatched_inspected_artifact_identity() -> None:
|
||||
class ArtifactPort(_Port):
|
||||
async def inspect_artifact(self, **params: Any) -> object:
|
||||
return {
|
||||
"id": "other",
|
||||
"version": 2,
|
||||
"title": "Other",
|
||||
"kind": "workflow",
|
||||
"description": None,
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
"output_schema": {"type": "object", "properties": {}},
|
||||
"outcomes": ["ok"],
|
||||
"plan": {
|
||||
"name": "other",
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
"state_schema": {"type": "object", "properties": {}},
|
||||
"output_schema": {"type": "object", "properties": {}},
|
||||
"outcomes": ["ok"],
|
||||
"start": "done",
|
||||
"nodes": [{"id": "done", "type": "end", "outcome": "ok"}],
|
||||
"edges": [],
|
||||
},
|
||||
"required_capabilities": [],
|
||||
"workflow_dependencies": {},
|
||||
"created_from_catalog_version": None,
|
||||
}
|
||||
|
||||
app = App._from_port(cast(WorkflowClientPort, ArtifactPort()))
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.artifacts.inspect"):
|
||||
await app.workflow("report", version=1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_capability_discovery_returns_rich_page() -> None:
|
||||
page = await _app().capabilities(query="search", limit=10)
|
||||
|
||||
@@ -6,6 +6,7 @@ import pytest
|
||||
|
||||
from wf_authoring import WorkflowBuilder
|
||||
from wf_client import App, ArtifactRef, EditableWorkflow, RemoteCapability
|
||||
from wf_client.errors import InvalidResponse
|
||||
from wf_client.protocols import WorkflowClientPort
|
||||
from wf_platform import CapabilityRef
|
||||
|
||||
@@ -20,6 +21,7 @@ class FakePort:
|
||||
"workflow_dependencies": {},
|
||||
}
|
||||
self.inspect_artifact_result: dict[str, Any] | None = None
|
||||
self.create_artifact_result: dict[str, Any] | None = None
|
||||
|
||||
async def validate_artifact_plan(self, **params: Any) -> object:
|
||||
self.calls.append(("validate_artifact_plan", params))
|
||||
@@ -27,7 +29,11 @@ class FakePort:
|
||||
|
||||
async def create_artifact_from_plan(self, **params: Any) -> object:
|
||||
self.calls.append(("create_artifact_from_plan", params))
|
||||
return {"artifact_id": params["artifact_id"], "version": params["version"], "saved": True}
|
||||
return self.create_artifact_result or {
|
||||
"artifact_id": params["artifact_id"],
|
||||
"version": params["version"],
|
||||
"saved": True,
|
||||
}
|
||||
|
||||
async def inspect_artifact(self, **params: Any) -> object:
|
||||
self.calls.append(("inspect_artifact", params))
|
||||
@@ -43,13 +49,22 @@ def valid_plan(version: int = 1) -> dict[str, Any]:
|
||||
"kind": "workflow",
|
||||
"description": None,
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
"output_schema": {"type": "object", "properties": {"value": {"type": "string"}}},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
},
|
||||
"outcomes": ["ok"],
|
||||
"plan": {
|
||||
"name": "report",
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
"state_schema": {"type": "object", "properties": {"value": {"type": "string"}}},
|
||||
"output_schema": {"type": "object", "properties": {"value": {"type": "string"}}},
|
||||
"state_schema": {
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
},
|
||||
"output_schema": {
|
||||
"type": "object",
|
||||
"properties": {"value": {"type": "string"}},
|
||||
},
|
||||
"outcomes": ["ok"],
|
||||
"output": [{"path": "state.value", "target": "value"}],
|
||||
"start": "done",
|
||||
@@ -75,9 +90,7 @@ def remote_plan_without_schema_snapshots(version: int = 1) -> dict[str, Any]:
|
||||
{"id": "done", "type": "end", "outcome": "ok"},
|
||||
]
|
||||
payload["plan"]["start"] = "remote"
|
||||
payload["plan"]["edges"] = [
|
||||
{"from": "remote", "outcome": "ok", "to": "done"}
|
||||
]
|
||||
payload["plan"]["edges"] = [{"from": "remote", "outcome": "ok", "to": "done"}]
|
||||
payload["required_capabilities"] = [
|
||||
{
|
||||
"ref": {"source": "app.default", "capability_key": "remote"},
|
||||
@@ -155,19 +168,73 @@ async def test_edit_and_save_inspects_exact_saved_version() -> None:
|
||||
graph = await app.edit_workflow("report", version=1)
|
||||
assert isinstance(graph, WorkflowBuilder)
|
||||
assert isinstance(graph, EditableWorkflow)
|
||||
assert all(hasattr(graph, name) for name in ("when", "choose", "match", "foreach", "interrupt", "end", "connect", "set_entry_point"))
|
||||
assert all(
|
||||
hasattr(graph, name)
|
||||
for name in (
|
||||
"when",
|
||||
"choose",
|
||||
"match",
|
||||
"foreach",
|
||||
"interrupt",
|
||||
"end",
|
||||
"connect",
|
||||
"set_entry_point",
|
||||
)
|
||||
)
|
||||
|
||||
port.inspect_artifact_result = valid_plan(version=2)
|
||||
saved = await graph.save(version=2)
|
||||
|
||||
create = next(params for operation, params in port.calls if operation == "create_artifact_from_plan")
|
||||
create = next(
|
||||
params
|
||||
for operation, params in port.calls
|
||||
if operation == "create_artifact_from_plan"
|
||||
)
|
||||
assert create["plan"] == valid_plan(version=1)["plan"]
|
||||
inspect = [params for operation, params in port.calls if operation == "inspect_artifact"][-1]
|
||||
inspect = [
|
||||
params for operation, params in port.calls if operation == "inspect_artifact"
|
||||
][-1]
|
||||
assert inspect == {"artifact_id": "report", "version": 2}
|
||||
assert saved.ref == ArtifactRef("report", 2)
|
||||
assert str(saved.workflow.output[0].target) == "value"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_rejects_mismatched_create_acknowledgement() -> None:
|
||||
port = FakePort()
|
||||
graph = App._from_port(cast(WorkflowClientPort, port)).new_workflow(
|
||||
"report",
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
state_schema={"type": "object", "properties": {}},
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
)
|
||||
graph.set_entry_point(graph.end("ok", id="done"))
|
||||
port.create_artifact_result = {
|
||||
"artifact_id": "other",
|
||||
"version": 2,
|
||||
"saved": True,
|
||||
}
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.artifacts.create_from_plan"):
|
||||
await graph.save(version=2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_rejects_mismatched_exact_inspection() -> None:
|
||||
port = FakePort()
|
||||
graph = App._from_port(cast(WorkflowClientPort, port)).new_workflow(
|
||||
"report",
|
||||
input_schema={"type": "object", "properties": {}},
|
||||
state_schema={"type": "object", "properties": {}},
|
||||
output_schema={"type": "object", "properties": {}},
|
||||
)
|
||||
graph.set_entry_point(graph.end("ok", id="done"))
|
||||
port.inspect_artifact_result = valid_plan(version=3)
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.artifacts.inspect"):
|
||||
await graph.save(version=2)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_editable_artifact_without_schema_snapshots_remains_saveable() -> None:
|
||||
port = FakePort()
|
||||
|
||||
@@ -36,14 +36,18 @@ def _inspect_payload() -> dict[str, Any]:
|
||||
class _Port:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
self.result_qualified_name = "app.default.search"
|
||||
self.result_source_id = "app.default"
|
||||
self.result_kind = "node_spec"
|
||||
self.result_deployment_id: str | None = None
|
||||
|
||||
async def call_capability(self, **params: Any) -> object:
|
||||
self.calls.append(params)
|
||||
return {
|
||||
"qualified_name": "app.default.search",
|
||||
"source_id": "app.default",
|
||||
"kind": "node_spec",
|
||||
"deployment_id": None,
|
||||
"qualified_name": self.result_qualified_name,
|
||||
"source_id": self.result_source_id,
|
||||
"kind": self.result_kind,
|
||||
"deployment_id": self.result_deployment_id,
|
||||
"outcome": "ok",
|
||||
"output": {"results": ["one"]},
|
||||
"diagnostics": [],
|
||||
@@ -118,6 +122,67 @@ async def test_remote_capability_rejects_mixed_payload_forms() -> None:
|
||||
await capability({"query": "workflow"}, query="again")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remote_capability_rejects_mismatched_call_source() -> None:
|
||||
port = _Port()
|
||||
port.result_source_id = "other.source"
|
||||
capability = RemoteCapability(
|
||||
_port=cast(WorkflowClientPort, port),
|
||||
ref=CapabilityRef.parse("app.default.search"),
|
||||
qualified_name="app.default.search",
|
||||
description=None,
|
||||
input_schema={"type": "object"},
|
||||
output_schema={"type": "object"},
|
||||
outcomes=("ok",),
|
||||
is_async=False,
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.capabilities.call"):
|
||||
await capability({})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_node_capability_rejects_unexpected_result_deployment() -> None:
|
||||
port = _Port()
|
||||
port.result_deployment_id = "unexpected"
|
||||
capability = RemoteCapability(
|
||||
_port=cast(WorkflowClientPort, port),
|
||||
ref=CapabilityRef.parse("app.default.search"),
|
||||
qualified_name="app.default.search",
|
||||
description=None,
|
||||
input_schema={"type": "object"},
|
||||
output_schema={"type": "object"},
|
||||
outcomes=("ok",),
|
||||
is_async=False,
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.capabilities.call"):
|
||||
await capability.call({}, deployment_id="ignored-by-node-spec")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_wrapper_capability_requires_exact_result_deployment() -> None:
|
||||
port = _Port()
|
||||
port.result_qualified_name = "workflow.report.v1"
|
||||
port.result_source_id = "workflow"
|
||||
port.result_kind = "wrapper_artifact"
|
||||
port.result_deployment_id = "other.deployment"
|
||||
capability = RemoteCapability(
|
||||
_port=cast(WorkflowClientPort, port),
|
||||
ref=CapabilityRef(source=SourceRef.parse("workflow"), name="report.v1"),
|
||||
qualified_name="workflow.report.v1",
|
||||
description=None,
|
||||
input_schema={"type": "object"},
|
||||
output_schema={"type": "object"},
|
||||
outcomes=("ok",),
|
||||
is_async=False,
|
||||
_kind="wrapper_artifact",
|
||||
)
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.capabilities.call"):
|
||||
await capability.call({}, deployment_id="report.production")
|
||||
|
||||
|
||||
def test_remote_capability_rejects_invalid_inspected_schema() -> None:
|
||||
with pytest.raises(InvalidResponse, match="invalid JSON Schema"):
|
||||
RemoteCapability(
|
||||
|
||||
@@ -5,8 +5,9 @@ from typing import Any, cast
|
||||
import pytest
|
||||
|
||||
from wf_artifacts import WorkflowArtifact as ArtifactModel
|
||||
from wf_client import DeploymentRequired, WorkflowClientPort
|
||||
from wf_client import DeploymentRequired
|
||||
from wf_client.errors import DeploymentNotRunnable, InvalidResponse
|
||||
from wf_client.protocols import WorkflowClientPort
|
||||
from wf_client.workflows import WorkflowArtifact
|
||||
from wf_core import Workflow
|
||||
|
||||
@@ -193,6 +194,16 @@ async def test_artifact_deploy_rejects_wrong_created_deployment_id() -> None:
|
||||
await artifact.deploy("report.production")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_artifact_deploy_rejects_wrong_created_artifact_identity() -> None:
|
||||
artifact = _artifact()
|
||||
port = cast(_FakePort, artifact._port)
|
||||
port.save_result["artifact_version"] = 2
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.deployments.save"):
|
||||
await artifact.deploy("report.production")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_artifact_deploy_rejects_wrong_inspected_deployment_id() -> None:
|
||||
artifact = _artifact()
|
||||
@@ -310,3 +321,47 @@ async def test_deployment_run_preserves_server_error_and_diagnostics() -> None:
|
||||
assert captured.value.error == "dependency check failed"
|
||||
assert captured.value.outcome == "rejected"
|
||||
assert captured.value.diagnostics[0].code == "missing_source"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_artifact_snapshot_defensively_copies_nested_models() -> None:
|
||||
artifact = _artifact()
|
||||
|
||||
exposed_artifact = artifact.artifact
|
||||
exposed_workflow = artifact.workflow
|
||||
exposed_artifact.id = "mutated"
|
||||
exposed_artifact.plan["name"] = "mutated"
|
||||
exposed_workflow.name = "mutated"
|
||||
|
||||
assert artifact.ref.artifact_id == "report"
|
||||
assert artifact.inspect().name == "report"
|
||||
assert artifact.edit().name == "report"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_deployment_snapshot_defensively_copies_model_and_diagnostics() -> None:
|
||||
artifact = _artifact()
|
||||
port = cast(_FakePort, artifact._port)
|
||||
port.validation_result["diagnostics"] = [
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "drift",
|
||||
"logical_ref": "app.default",
|
||||
"bound_source": "company.production",
|
||||
"message": "original",
|
||||
"repair_hint": None,
|
||||
}
|
||||
]
|
||||
deployment = await artifact.deploy("report.production")
|
||||
|
||||
exposed_model = deployment.model
|
||||
exposed_diagnostics = deployment.diagnostics
|
||||
exposed_model.id = "mutated"
|
||||
exposed_model.bindings = []
|
||||
exposed_diagnostics[0].message = "mutated"
|
||||
|
||||
assert deployment.deployment_id == "report.production"
|
||||
assert deployment.bindings == {"app.default": "company.production"}
|
||||
assert deployment.diagnostics[0].message == "original"
|
||||
await deployment.run({})
|
||||
assert port.calls[-1][1]["deployment_id"] == "report.production"
|
||||
|
||||
@@ -126,7 +126,8 @@ def test_repr_does_not_materialize_an_unbounded_iterable() -> None:
|
||||
|
||||
|
||||
def test_all_rich_objects_render_without_port_access() -> None:
|
||||
port = cast(WorkflowClientPort, _port())
|
||||
raw_port = _port()
|
||||
port = cast(WorkflowClientPort, raw_port)
|
||||
diagnostic = WorkflowDiagnostic("error", "bad", "state.x", "broken")
|
||||
local = ValidationReport()
|
||||
objects = [
|
||||
@@ -153,4 +154,4 @@ def test_all_rich_objects_render_without_port_access() -> None:
|
||||
},
|
||||
)
|
||||
)
|
||||
assert port.calls == []
|
||||
assert raw_port.calls == []
|
||||
|
||||
@@ -4,8 +4,9 @@ from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
|
||||
from wf_client import App, Run, WorkflowClientPort
|
||||
from wf_client import App, Run
|
||||
from wf_client.errors import DeploymentNotRunnable, InvalidResponse
|
||||
from wf_client.protocols import WorkflowClientPort
|
||||
|
||||
|
||||
def _payload(
|
||||
@@ -135,6 +136,22 @@ async def test_resume_rejects_mismatched_result_id() -> None:
|
||||
await run.resume({"approved": True})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("operation", ["refresh", "resume"])
|
||||
async def test_run_lifecycle_rejects_mismatched_deployment_identity(
|
||||
operation: str,
|
||||
) -> None:
|
||||
port = _Port()
|
||||
port.resume_payload["deployment_id"] = "other.deployment"
|
||||
run = Run.from_payload(cast(WorkflowClientPort, port), _payload())
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.runs"):
|
||||
if operation == "refresh":
|
||||
await run.refresh()
|
||||
else:
|
||||
await run.resume({"approved": True})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_malformed_interrupt_route_is_invalid_response() -> None:
|
||||
payload = _payload()
|
||||
@@ -180,3 +197,59 @@ async def test_trace_rejects_invalid_bounds_before_io() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
await run.trace(limit=101)
|
||||
assert port.calls == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("field", "value"),
|
||||
[
|
||||
("run_id", "other-run"),
|
||||
("deployment_id", "other.deployment"),
|
||||
("trace_start", 1),
|
||||
("trace_limit", 26),
|
||||
],
|
||||
)
|
||||
async def test_trace_rejects_mismatched_identity_or_page(
|
||||
field: str,
|
||||
value: object,
|
||||
) -> None:
|
||||
port = _Port()
|
||||
port.trace_payload[field] = value
|
||||
run = Run.from_payload(cast(WorkflowClientPort, port), _payload())
|
||||
|
||||
with pytest.raises(InvalidResponse, match="workflow.runs.trace"):
|
||||
await run.trace(start=0, limit=25)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_snapshot_defensively_copies_nested_public_values() -> None:
|
||||
port = _Port()
|
||||
payload = _payload()
|
||||
payload["output"] = {"nested": {"value": "original"}}
|
||||
payload["diagnostics"] = [
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "drift",
|
||||
"logical_ref": "app.default",
|
||||
"bound_source": "company.production",
|
||||
"message": "original",
|
||||
"repair_hint": None,
|
||||
}
|
||||
]
|
||||
run = Run.from_payload(cast(WorkflowClientPort, port), payload)
|
||||
|
||||
exposed_output = run.output
|
||||
exposed_interrupt = run.interrupt
|
||||
exposed_diagnostics = run.diagnostics
|
||||
assert exposed_output is not None
|
||||
assert exposed_interrupt is not None
|
||||
exposed_output["nested"]["value"] = "mutated"
|
||||
exposed_interrupt.payload["question"] = "mutated"
|
||||
exposed_diagnostics[0].message = "mutated"
|
||||
|
||||
assert run.output == {"nested": {"value": "original"}}
|
||||
assert run.interrupt is not None
|
||||
assert run.interrupt.payload == {"question": "approve?"}
|
||||
assert run.diagnostics[0].message == "original"
|
||||
await run.resume({"approved": True})
|
||||
assert port.calls[-1][1]["run_id"] == "run-1"
|
||||
|
||||
@@ -18,7 +18,9 @@ def _manifest() -> ContractManifest:
|
||||
return manifest_from_openrpc(synthetic_openrpc_document())
|
||||
|
||||
|
||||
def test_write_generates_once_and_writes_requested_contract(monkeypatch, tmp_path: Path) -> None:
|
||||
def test_write_generates_once_and_writes_requested_contract(
|
||||
monkeypatch, tmp_path: Path
|
||||
) -> None:
|
||||
manifest = _manifest()
|
||||
calls: list[tuple[object, Path]] = []
|
||||
generate_calls = 0
|
||||
@@ -35,7 +37,10 @@ def test_write_generates_once_and_writes_requested_contract(monkeypatch, tmp_pat
|
||||
"wf_contract_manifest.__main__.write_manifest",
|
||||
lambda value, path: calls.append((value, path)) or path,
|
||||
)
|
||||
monkeypatch.setattr("wf_contract_manifest.__main__.DEFAULT_MANIFEST_PATH", tmp_path / "manifest.json")
|
||||
monkeypatch.setattr(
|
||||
"wf_contract_manifest.__main__.DEFAULT_MANIFEST_PATH",
|
||||
tmp_path / "manifest.json",
|
||||
)
|
||||
|
||||
assert main(["write"]) == 0
|
||||
assert calls == [(manifest, tmp_path / "manifest.json")]
|
||||
|
||||
@@ -151,9 +151,7 @@ def test_manifest_separates_recursive_step_inputs_from_workflow_outputs() -> Non
|
||||
input_binding_schema = schemas["InputExpressionBinding"]
|
||||
properties = input_binding_schema.get("properties")
|
||||
assert isinstance(properties, dict)
|
||||
assert properties["expression"] == {
|
||||
"$ref": "#/components/schemas/InputExpression"
|
||||
}
|
||||
assert properties["expression"] == {"$ref": "#/components/schemas/InputExpression"}
|
||||
expression_schema = schemas["InputExpression"]
|
||||
assert expression_schema["discriminator"] == {
|
||||
"mapping": {
|
||||
|
||||
@@ -177,9 +177,7 @@ def test_interrupted_saved_child_blocks_resume_until_pinned_source_returns(
|
||||
assert blocked["status"] == "interrupted"
|
||||
assert blocked["resume_readiness"] == "blocked"
|
||||
assert blocked["diagnostics"][0]["code"] == "source_disabled"
|
||||
assert (
|
||||
run_store.get_run(paused_run_id).resume_readiness is ResumeReadiness.BLOCKED
|
||||
)
|
||||
assert run_store.get_run(paused_run_id).resume_readiness is ResumeReadiness.BLOCKED
|
||||
assert run_store.get_latest_checkpoint(paused_run_id).sequence == 1
|
||||
|
||||
handlers.service.capability_sources["demo.personal"].enabled = True
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
@@ -32,25 +31,6 @@ 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(
|
||||
@@ -354,8 +334,8 @@ async def test_rpc_workflow_client_lists_inspects_validates_and_deletes_deployme
|
||||
|
||||
|
||||
async def test_rpc_workflow_client_draft_workspace_lifecycle(tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
@@ -550,8 +530,8 @@ async def test_rpc_client_sends_exact_replace_document_payload() -> None:
|
||||
|
||||
|
||||
async def test_rpc_client_builds_capability_free_draft_lifecycle(tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
@@ -625,8 +605,8 @@ def test_rpc_client_satisfies_draft_surface_static_shape() -> None:
|
||||
|
||||
|
||||
async def test_rpc_workflow_client_deletes_draft_workspace(tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
@@ -772,8 +752,8 @@ async def test_rpc_client_validates_artifact_plan_without_persisting(tmp_path) -
|
||||
|
||||
|
||||
async def test_rpc_client_set_workflow_output_map(tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
@@ -807,8 +787,8 @@ async def test_rpc_client_set_workflow_output_map(tmp_path) -> None:
|
||||
|
||||
|
||||
async def test_rpc_client_draft_workspace_focused_edit_methods(tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
@@ -1069,8 +1049,8 @@ async def test_rpc_client_draft_remove_methods(tmp_path) -> None:
|
||||
|
||||
|
||||
async def test_rpc_client_draft_workspace_add_step_from_capability(tmp_path) -> None:
|
||||
server = build_local_static_workflow_server(tmp_path / "store")
|
||||
app = create_rpc_app(server)
|
||||
server = build_local_static_workflow_server(tmp_path / "store", drafts=True)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport, base_url="http://test"
|
||||
|
||||
@@ -200,7 +200,7 @@ def _runtime_reuse_server(
|
||||
],
|
||||
)
|
||||
store_roots = config.store_roots
|
||||
workflow_stores = file_workflow_stores(store_roots.workflow_root)
|
||||
workflow_stores = file_workflow_stores(store_roots.workflow_root, drafts=True)
|
||||
auth_store = FileAuthStore(store_roots.auth_root)
|
||||
catalog_store = FileCatalogStore(store_roots.catalog_cache_root)
|
||||
factory = _RecordingSessionFactory()
|
||||
@@ -232,7 +232,7 @@ async def test_mcp_backed_rpc_lists_and_mutates_source_registry(tmp_path) -> Non
|
||||
SourceRegistryFile(sources=[_registry_entry("demo.registry")])
|
||||
)
|
||||
server = build_workflow_server_from_config(config)
|
||||
app = create_rpc_app(server)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
@@ -255,7 +255,7 @@ async def test_mcp_backed_rpc_lists_and_mutates_source_registry(tmp_path) -> Non
|
||||
async def test_mcp_backed_rpc_capability_list_filters_by_source(tmp_path) -> None:
|
||||
config = BrokerConfig(store_root=tmp_path / "store", connections=[])
|
||||
server = build_workflow_server_from_config(config)
|
||||
app = create_rpc_app(server)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
@@ -285,7 +285,7 @@ async def test_mcp_backed_rpc_reports_connections_and_events(tmp_path) -> None:
|
||||
],
|
||||
)
|
||||
server = build_workflow_server_from_config(config)
|
||||
app = create_rpc_app(server)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
@@ -299,7 +299,7 @@ async def test_mcp_backed_rpc_reports_connections_and_events(tmp_path) -> None:
|
||||
async def test_mcp_backed_rpc_applies_source_registry_changes(tmp_path) -> None:
|
||||
config = BrokerConfig(store_root=tmp_path / "store", connections=[])
|
||||
server = build_workflow_server_from_config(config)
|
||||
app = create_rpc_app(server)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=app),
|
||||
@@ -368,7 +368,7 @@ async def test_mcp_backed_rpc_can_be_built_from_neutral_workflow_config(
|
||||
}
|
||||
)
|
||||
server = build_workflow_server_from_workflow_config(workflow_config)
|
||||
app = create_rpc_app(server)
|
||||
app = create_rpc_app(server, drafts=True)
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
@@ -408,7 +408,7 @@ async def test_mcp_backed_rpc_resumes_interrupted_run_after_server_rebuild(
|
||||
}
|
||||
)
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(first_server)),
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(first_server, drafts=True)),
|
||||
base_url="http://test",
|
||||
) as http_client:
|
||||
first_client = RpcWorkflowApiClient(
|
||||
@@ -433,7 +433,7 @@ async def test_mcp_backed_rpc_resumes_interrupted_run_after_server_rebuild(
|
||||
|
||||
rebuilt_server = build_workflow_server_from_workflow_config(workflow_config)
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(rebuilt_server)),
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(rebuilt_server, drafts=True)),
|
||||
base_url="http://test",
|
||||
) as http_client:
|
||||
rebuilt_client = RpcWorkflowApiClient(
|
||||
@@ -463,7 +463,7 @@ async def test_mcp_backed_rpc_workflow_reuses_runtime_session_across_runs(
|
||||
assert factory.created_connections[0].id == "fixture.default"
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server)),
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server, drafts=True)),
|
||||
base_url="http://test",
|
||||
) as http_client:
|
||||
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
|
||||
@@ -597,7 +597,7 @@ async def test_mcp_backed_rpc_workflow_reuses_runtime_session_direct_setup(
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server)),
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server, drafts=True)),
|
||||
base_url="http://test",
|
||||
) as http_client:
|
||||
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
|
||||
@@ -696,7 +696,7 @@ async def test_mcp_backed_rpc_deployment_becomes_unrunnable_after_source_removed
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server)),
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server, drafts=True)),
|
||||
base_url="http://test",
|
||||
) as http_client:
|
||||
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
|
||||
@@ -748,7 +748,7 @@ async def test_mcp_backed_rpc_workflow_reuses_real_stdio_fixture_session(
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server)),
|
||||
transport=httpx.ASGITransport(app=create_rpc_app(server, drafts=True)),
|
||||
base_url="http://test",
|
||||
) as http_client:
|
||||
client = RpcWorkflowApiClient(url="http://test/rpc", http_client=http_client)
|
||||
|
||||
Reference in New Issue
Block a user