mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 11:40:32 +00:00
feat(SKY-8879) copilot-stack/06: MCP tools surface + orphan-task cancellation (#5517)
This commit is contained in:
parent
d58ea46163
commit
faa2b233cb
16 changed files with 2588 additions and 23 deletions
|
|
@ -6,10 +6,14 @@
|
|||
- ``execute_workflow_webhook`` returns cleanly when the workflow row has been
|
||||
soft-deleted mid-run — it must not raise ``WorkflowNotFound`` from the
|
||||
cleanup path.
|
||||
- The cancellation-safe finalize pattern used in ``execute_workflow``'s outer
|
||||
``finally`` runs ``_finalize_workflow_run_status`` via ``asyncio.shield``
|
||||
so an outer cancel mid-body still restores the real terminal status.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
|
@ -113,3 +117,89 @@ async def test_build_status_response_uses_filter_deleted_false_when_allowed(
|
|||
|
||||
by_wpid.assert_awaited_once()
|
||||
assert by_wpid.call_args.kwargs["filter_deleted"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shielded_finalize_runs_when_outer_cancelled_mid_body() -> None:
|
||||
"""Contract test for the ``execute_workflow`` cancellation-safe pattern:
|
||||
when the try body is cancelled after ``pre_finally_status`` is captured,
|
||||
the outer ``finally`` must still run ``_finalize_workflow_run_status``
|
||||
via ``asyncio.shield`` so the row ends up terminal rather than stuck as
|
||||
transient ``running``. Mirrors the structure of
|
||||
``WorkflowService.execute_workflow``; if anyone removes the ``shield`` or
|
||||
moves finalize back into the try, this test breaks.
|
||||
"""
|
||||
|
||||
finalize_calls: list[WorkflowRunStatus] = []
|
||||
clean_up_called = False
|
||||
body_entered = asyncio.Event()
|
||||
|
||||
async def finalize(status: WorkflowRunStatus) -> None:
|
||||
# Simulate a non-trivial DB write so shield cancellation-protection
|
||||
# matters rather than being invisible.
|
||||
await asyncio.sleep(0.05)
|
||||
finalize_calls.append(status)
|
||||
|
||||
async def clean_up() -> None:
|
||||
nonlocal clean_up_called
|
||||
clean_up_called = True
|
||||
|
||||
async def simulated_execute_workflow() -> None:
|
||||
pre_finally_status: WorkflowRunStatus | None = None
|
||||
try:
|
||||
pre_finally_status = WorkflowRunStatus.failed
|
||||
body_entered.set()
|
||||
# Simulate the finally-block execution phase that our copilot
|
||||
# cancel lands inside of.
|
||||
await asyncio.sleep(10)
|
||||
finally:
|
||||
if pre_finally_status is not None:
|
||||
try:
|
||||
await asyncio.shield(finalize(pre_finally_status))
|
||||
except Exception:
|
||||
pass
|
||||
await clean_up()
|
||||
|
||||
task = asyncio.create_task(simulated_execute_workflow())
|
||||
await body_entered.wait()
|
||||
task.cancel()
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
assert finalize_calls == [WorkflowRunStatus.failed], (
|
||||
"shielded finalize must run with the captured pre_finally_status"
|
||||
)
|
||||
assert clean_up_called, "clean_up_workflow must still run in the outer finally"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_shielded_finalize_skipped_when_pre_finally_status_unset() -> None:
|
||||
"""If cancellation lands before block execution captures
|
||||
``pre_finally_status``, there's no intended terminal state to restore —
|
||||
the outer ``finally`` must skip finalize, not call it with ``None``.
|
||||
"""
|
||||
|
||||
finalize_called = False
|
||||
|
||||
async def finalize(status: WorkflowRunStatus) -> None:
|
||||
nonlocal finalize_called
|
||||
finalize_called = True
|
||||
|
||||
async def simulated_execute_workflow() -> None:
|
||||
pre_finally_status: WorkflowRunStatus | None = None
|
||||
try:
|
||||
await asyncio.sleep(10)
|
||||
pre_finally_status = WorkflowRunStatus.failed # pragma: no cover
|
||||
finally:
|
||||
if pre_finally_status is not None:
|
||||
await asyncio.shield(finalize(pre_finally_status))
|
||||
|
||||
task = asyncio.create_task(simulated_execute_workflow())
|
||||
await asyncio.sleep(0) # let the task enter its body
|
||||
task.cancel()
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
|
||||
assert not finalize_called, "finalize must not run when pre_finally_status is unset"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue