"""Durable resume-attempt marker with attempt identities (T09).""" 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, 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(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 isinstance(context.run_store, FileRunStore) return WorkflowRunApi(context), context.run_store def test_resume_attempt_ids_are_monotonic_across_store_instances( tmp_path: Path, ) -> None: api, store = _api(tmp_path / "resume-marker") started = asyncio.run( api.run_deployment(deployment_id="echo.personal", workflow_input={"text": "hi"}) ) run_id = started["run_id"] assert run_id is not None # Interrupt the run via API resume with an interrupt outcome? Echo runs to # completion; instead verify the marker lifecycle on a synthetic # interrupted record through the store seam. first = store.allocate_resume_attempt_id() second = FileRunStore(store.root).allocate_resume_attempt_id() assert second == first + 1 def test_active_attempt_marker_is_durable(tmp_path: Path) -> None: from datetime import UTC, datetime from wf_artifacts.runs.models import ResumeAttempt api, store = _api(tmp_path / "active-block") started = asyncio.run( api.run_deployment(deployment_id="echo.personal", workflow_input={"text": "hi"}) ) run_id = started["run_id"] assert run_id is not None now = datetime.now(UTC) store.save_resume_attempt( ResumeAttempt( run_id=run_id, attempt_id=store.allocate_resume_attempt_id(), state="ACTIVE", created_at=now, updated_at=now, ) ) # A second resume while ACTIVE is ambiguous and must fail closed without # executing. Echo runs complete immediately so restore fails first on # non-interrupted status; the ACTIVE guard is exercised on interrupted # runs in recovery tests (T10). reopened = FileRunStore(store.root) attempt = reopened.get_resume_attempt(run_id) assert attempt is not None assert attempt.state == "ACTIVE" def test_stopped_checkpoint_echoes_attempt_id(tmp_path: Path) -> None: from tests.artifacts.test_run_store import artifact as _artifact from tests.artifacts.test_run_store import deployment as _deployment from wf_api.run_lifecycle import persist_stopped_run from wf_artifacts import PinnedRunEnvironment from wf_core import RunState, RunStatus store = FileRunStore(tmp_path) env = PinnedRunEnvironment( deployment=_deployment(), root_artifact=_artifact(), child_artifacts=[], ) run = RunState( workflow_name="parent", status=RunStatus.COMPLETED, workflow_input={}, state={}, ) record = persist_stopped_run( store=store, environment=env, run=run, run_id=None, attempt_id=7 ) checkpoint = store.get_latest_checkpoint(record.id) assert checkpoint.attempt_id == 7