118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
"""Admission path through the run API: persist before dispatch (T06)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
from tests.wf_mcp.test_support import echo_tool
|
|
from tests.wf_mcp.workflow_surface.conftest import echo_artifact
|
|
from wf_api.runs import WorkflowRunApi
|
|
from wf_artifacts import (
|
|
FileRunStore,
|
|
FileWorkflowArtifactStore,
|
|
StoredRunStatus,
|
|
WorkflowDeployment,
|
|
)
|
|
from wf_mcp.broker import WfMcpService
|
|
from wf_mcp.broker.service.workflow_operation_context import context_from_service
|
|
from wf_mcp.models import ConnectionConfig
|
|
from wf_mcp.storage import FileStore
|
|
|
|
|
|
def _api_with_echo(root: Path) -> tuple[WorkflowRunApi, FileRunStore]:
|
|
|
|
artifact_store = FileWorkflowArtifactStore(root)
|
|
artifact_store.save_artifact(echo_artifact())
|
|
artifact_store.save_deployment(
|
|
WorkflowDeployment(
|
|
id="echo.personal",
|
|
artifact_id="echo",
|
|
artifact_version=1,
|
|
bindings=[{"logical_source": "demo", "concrete_source": "demo.personal"}],
|
|
)
|
|
)
|
|
service = WfMcpService(
|
|
store=FileStore(root / "mcp"),
|
|
artifact_store=artifact_store,
|
|
run_store=FileRunStore(root / "mcp"),
|
|
)
|
|
service.register_connection(
|
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
|
)
|
|
service.register_specs("demo.personal", echo_tool)
|
|
context = context_from_service(service)
|
|
assert context.run_store is not None
|
|
assert isinstance(context.run_store, FileRunStore)
|
|
return WorkflowRunApi(context), context.run_store
|
|
|
|
|
|
def test_manual_run_persists_admission_before_dispatch(tmp_path: Path) -> None:
|
|
api, store = _api_with_echo(tmp_path / "admit")
|
|
result = asyncio.run(
|
|
api.run_deployment(
|
|
deployment_id="echo.personal",
|
|
workflow_input={"text": "hi"},
|
|
)
|
|
)
|
|
assert result["run_id"] is not None
|
|
admission = store.get_admission(result["run_id"])
|
|
assert admission.resolved_input == {"text": "hi"}
|
|
assert admission.environment.deployment.id == "echo.personal"
|
|
record = store.get_run(result["run_id"])
|
|
assert record.status is not StoredRunStatus.ADMITTED
|
|
assert record.latest_checkpoint_id is not None
|
|
|
|
|
|
def test_fault_before_admission_never_dispatches(tmp_path: Path, monkeypatch) -> None:
|
|
import wf_api.runs as runs_module
|
|
|
|
api, store = _api_with_echo(tmp_path / "fault-before")
|
|
real_write = store._write_json
|
|
dispatched: list[str] = []
|
|
real_plan = runs_module.raw_plan_from_artifact
|
|
|
|
def _fail_on_admission(path: Path, payload: object) -> None:
|
|
if path.name == "admission.json":
|
|
raise OSError("injected admission failure")
|
|
real_write(path, payload)
|
|
|
|
def _spy_plan(artifact): # type: ignore[no-untyped-def]
|
|
dispatched.append("dispatch")
|
|
return real_plan(artifact)
|
|
|
|
# Real serialization fault at the admission file: nothing after it
|
|
# (plan building, dispatch, stopped persist) may run.
|
|
monkeypatch.setattr(store, "_write_json", _fail_on_admission)
|
|
monkeypatch.setattr(runs_module, "raw_plan_from_artifact", _spy_plan)
|
|
try:
|
|
asyncio.run(
|
|
api.run_deployment(
|
|
deployment_id="echo.personal",
|
|
workflow_input={"text": "hi"},
|
|
)
|
|
)
|
|
raise AssertionError("fault must propagate")
|
|
except OSError:
|
|
pass
|
|
assert dispatched == []
|
|
fresh = FileRunStore(store.root)
|
|
assert fresh.list_admissions() == []
|
|
assert fresh.list_runs() == []
|
|
|
|
|
|
def test_captured_invocation_freezes_input(tmp_path: Path) -> None:
|
|
api, store = _api_with_echo(tmp_path / "freeze")
|
|
payload = {"text": "hi", "nested": {"n": 1}}
|
|
result = asyncio.run(
|
|
api.run_deployment(
|
|
deployment_id="echo.personal",
|
|
workflow_input=payload,
|
|
)
|
|
)
|
|
assert result["run_id"] is not None
|
|
payload["text"] = "mutated"
|
|
payload["nested"]["n"] = 999
|
|
admission = store.get_admission(result["run_id"])
|
|
assert admission.resolved_input == {"text": "hi", "nested": {"n": 1}}
|