mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(task): restore pre-block owner when an admin override leaves blocked
A developer that hits a wall calls i_am_blocked, which escalates the code task
to its cell PM (assigned_to=PM, BLOCKED) and snapshots the dev as
pre_block_assignee — the intended dev->cell-PM triage handoff. The in-band
recovery (unblock(restore=True)) hands ownership back to the dev. But the
OUT-OF-BAND paths — the operator PATCH /tasks/{id} status override and the
orchestrator's own _auto_recover_blocked_parent / _auto_resume_paused_parent —
go through admin_set_status, which set only status and never restored the owner.
The task re-entered pending/in_progress still owned by the PM, and the dispatcher
then execute-spawned the PM on a code task it cannot do ('break this down and
delegate' against the task itself) -> respawn loop.
admin_set_status now, when taking a task out of 'blocked' into pending/in_progress
with a pre-block snapshot present, routes through the existing
_apply_pre_block_restore primitive (the same one unblock(restore=True) uses) to
hand ownership back to the executor. Every other override is unchanged, and the
escalate/apply_escalation/block-down path is untouched, so the dev->cell-PM
handoff still works. + 2 regression tests.
This commit is contained in:
@@ -543,6 +543,53 @@ async def test_unblock_no_branch_returns_to_pending() -> None:
|
||||
assert task.assigned_to == raiser
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_set_status_out_of_blocked_restores_pre_block_owner() -> None:
|
||||
# A code task a dev escalated to its cell PM (assigned_to=PM, BLOCKED,
|
||||
# snapshot=dev). Taking it out of blocked via the admin override (operator
|
||||
# PATCH, or the orchestrator's auto-recover/auto-resume) must hand ownership
|
||||
# back to the dev — otherwise it re-enters pending/in_progress still owned by
|
||||
# the PM and the dispatcher execute-spawns the PM on a dev code task (loop).
|
||||
dev = uuid4()
|
||||
pm = uuid4()
|
||||
task = _build_task(
|
||||
status=TaskStatus.BLOCKED,
|
||||
assigned_to=pm,
|
||||
claimed_by=pm,
|
||||
branch_name="feature/frontend/abc--def--ghi",
|
||||
pre_block_state="in_progress",
|
||||
pre_block_assignee=dev,
|
||||
)
|
||||
svc = TaskService(MagicMock(flush=AsyncMock()))
|
||||
_bind(svc, "get", AsyncMock(return_value=task))
|
||||
out = await svc.admin_set_status(task.id, TaskStatus.IN_PROGRESS)
|
||||
assert out is task
|
||||
assert task.status == TaskStatus.IN_PROGRESS
|
||||
assert task.assigned_to == dev
|
||||
assert task.claimed_by == dev
|
||||
assert task.pre_block_assignee is None
|
||||
assert task.pre_block_state is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_set_status_non_blocked_is_bare_status_set() -> None:
|
||||
# The restore branch fires ONLY on blocked -> pending/in_progress with a
|
||||
# snapshot. Every other override stays a plain status set; the owner is
|
||||
# untouched (no spurious restore/divert).
|
||||
owner = uuid4()
|
||||
task = _build_task(
|
||||
status=TaskStatus.AWAITING_PM_REVIEW,
|
||||
assigned_to=owner,
|
||||
claimed_by=owner,
|
||||
)
|
||||
svc = TaskService(MagicMock(flush=AsyncMock()))
|
||||
_bind(svc, "get", AsyncMock(return_value=task))
|
||||
out = await svc.admin_set_status(task.id, TaskStatus.COMPLETED)
|
||||
assert out is task
|
||||
assert task.status == TaskStatus.COMPLETED
|
||||
assert task.assigned_to == owner
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unblock_with_branch_resumes_in_progress() -> None:
|
||||
# A task claimed (has a branch) before it blocked resumes in_progress.
|
||||
|
||||
Reference in New Issue
Block a user