mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[chore] admin_set_status: attribute the blocked-restore to the admin actor + emit override row (#2176)
admin_set_status taking a BLOCKED task to pending/in_progress with a pre-block snapshot returned early via _apply_pre_block_restore, which emitted its audit row with agent_role=None and audit_agent_id=restored_owner (the pre-block dev) — the admin actor_id/actor_role were dropped entirely. Because this branch runs with force=false (pending/in_progress aren't hatch destinations), the distinguishing task.admin_override row (written only on the non-restore path, gated by force) was never written, so an operator could silently re-own a blocked task with no trace of who triggered it. Thread actor_id/actor_role into _apply_pre_block_restore (admin_set_status passes them with admin_override=True) so the transition audit row attributes the re-owning to the admin, and emit a task.admin_override row (forced=False, restore=True) on this branch independent of the force flag. The in-band unblock(restore=True) path passes no actor and keeps the legacy attribution (restored owner) with no override row. Test: admin PATCH status=pending on a BLOCKED task with a snapshot attributes every audit row to the admin (not the restored dev) and emits the override row.
This commit is contained in:
@@ -652,6 +652,54 @@ async def test_admin_set_status_no_force_emits_no_override_audit_row() -> None:
|
||||
assert not any(r.event_type == "task.admin_override" for r in rows)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_set_status_blocked_restore_attributes_admin_actor() -> None:
|
||||
"""#2176: an admin restore out of BLOCKED (with a pre-block snapshot) must
|
||||
stamp the ADMIN actor on the audit rows — not the restored owner — and emit
|
||||
a task.admin_override row so the re-owning is traceable. Previously this
|
||||
branch returned early via _apply_pre_block_restore with agent_role=None and
|
||||
audit_agent_id=restored_owner, and (force=false here) wrote no override row,
|
||||
so the privilege use was untraceable."""
|
||||
dev = uuid4()
|
||||
pm = uuid4()
|
||||
admin = uuid4()
|
||||
task = _build_task(
|
||||
status=TaskStatus.BLOCKED,
|
||||
assigned_to=pm,
|
||||
claimed_by=pm,
|
||||
branch_name="feature/backend/abc--def",
|
||||
pre_block_state="in_progress",
|
||||
pre_block_assignee=dev,
|
||||
)
|
||||
added: list[object] = []
|
||||
session = MagicMock()
|
||||
session.flush = AsyncMock()
|
||||
session.add.side_effect = added.append
|
||||
svc = TaskService(session)
|
||||
_bind(svc, "get", AsyncMock(return_value=task))
|
||||
|
||||
# force defaults to False — pending is not a hatch destination, so the only
|
||||
# way this override is recorded is the restore branch's own override row.
|
||||
out = await svc.admin_set_status(
|
||||
task.id, TaskStatus.PENDING, actor_id=admin, actor_role="ceo"
|
||||
)
|
||||
assert out is task
|
||||
assert task.status == TaskStatus.PENDING
|
||||
assert task.assigned_to == dev # ownership restored to the pre-block dev
|
||||
|
||||
rows = [r for r in added if isinstance(r, AuditLogTable)]
|
||||
override_rows = [r for r in rows if r.event_type == "task.admin_override"]
|
||||
assert len(override_rows) == 1
|
||||
override = override_rows[0]
|
||||
assert override.agent_id == admin # the admin, not the restored dev
|
||||
assert override.details["restore"] is True
|
||||
assert override.details["forced"] is False
|
||||
# Every audit row (the transition row AND the override row) is attributed to
|
||||
# the admin actor — the restored owner is NOT recorded as the actor.
|
||||
assert all(r.agent_id == admin for r in rows)
|
||||
assert not any(r.agent_id == dev for r in rows)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pre_block_restore_skips_revision_count_bump() -> None:
|
||||
"""#101 Gap B: restoring a blocked task to its snapshotted needs_revision
|
||||
|
||||
Reference in New Issue
Block a user