mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
"""`_detect_stuck_tasks` must skip CEO-held sources (release_manager / x_post /
|
|||
|
|
x_reply / video_post / PR-review / self-heal-pre-approve) before the age and
|
||
|
|
issues check. A held artifact sits PENDING by design until the CEO acts on it;
|
||
|
|
auto-blocking it on a "short description" wedges the held-artifact flow.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import UTC, datetime, timedelta
|
||
|
|
from typing import Any
|
||
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from roboco.runtime.orchestrator import AgentOrchestrator
|
||
|
|
from roboco.services.task import VIDEO_POST_SOURCE
|
||
|
|
|
||
|
|
|
||
|
|
def _make_orch() -> AgentOrchestrator:
|
||
|
|
"""Bare orchestrator without running __init__ (no settings I/O)."""
|
||
|
|
return AgentOrchestrator.__new__(AgentOrchestrator)
|
||
|
|
|
||
|
|
|
||
|
|
def _old_task(tid: str, source: str) -> dict[str, Any]:
|
||
|
|
"""A PENDING task older than the 10-minute stuck threshold with a short
|
||
|
|
description — exactly the shape that triggers _auto_block_task today."""
|
||
|
|
created = (datetime.now(UTC) - timedelta(minutes=30)).isoformat()
|
||
|
|
return {
|
||
|
|
"id": tid,
|
||
|
|
"source": source,
|
||
|
|
"status": "pending",
|
||
|
|
"description": "short", # < _MIN_DESCRIPTION_LEN (10)
|
||
|
|
"branch_name": None,
|
||
|
|
"assigned_to": None,
|
||
|
|
"estimated_complexity": "low",
|
||
|
|
"parent_task_id": None,
|
||
|
|
"created_at": created,
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_held_video_post_skipped_by_detect_stuck_tasks() -> None:
|
||
|
|
"""A PENDING video_post draft older than the threshold with a short
|
||
|
|
description must NOT be auto-blocked — it is a CEO-held artifact."""
|
||
|
|
orch = _make_orch()
|
||
|
|
held = _old_task("held-1", VIDEO_POST_SOURCE)
|
||
|
|
client: Any = MagicMock()
|
||
|
|
|
||
|
|
with (
|
||
|
|
patch.object(orch, "_fetch_tasks", new=AsyncMock(return_value=[held])),
|
||
|
|
patch.object(orch, "_check_dev_subtask_issue", new=AsyncMock(return_value=[])),
|
||
|
|
patch.object(orch, "_detect_sla_exceeded", new=AsyncMock()),
|
||
|
|
patch.object(orch, "_auto_block_task", new=AsyncMock()) as auto_block,
|
||
|
|
):
|
||
|
|
await orch._detect_stuck_tasks(client)
|
||
|
|
|
||
|
|
auto_block.assert_not_awaited()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_non_held_pending_task_still_auto_blocked() -> None:
|
||
|
|
"""A non-held PENDING task of the same age + short description is still
|
||
|
|
auto-blocked — the held-source skip must not over-widen."""
|
||
|
|
orch = _make_orch()
|
||
|
|
normal = _old_task("normal-1", "manual")
|
||
|
|
client: Any = MagicMock()
|
||
|
|
|
||
|
|
with (
|
||
|
|
patch.object(orch, "_fetch_tasks", new=AsyncMock(return_value=[normal])),
|
||
|
|
patch.object(orch, "_check_dev_subtask_issue", new=AsyncMock(return_value=[])),
|
||
|
|
patch.object(orch, "_detect_sla_exceeded", new=AsyncMock()),
|
||
|
|
patch.object(orch, "_auto_block_task", new=AsyncMock()) as auto_block,
|
||
|
|
):
|
||
|
|
await orch._detect_stuck_tasks(client)
|
||
|
|
|
||
|
|
auto_block.assert_awaited_once()
|
||
|
|
assert auto_block.await_args is not None
|
||
|
|
assert auto_block.await_args.args[1] == "normal-1"
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
pytest.main([__file__, "-q"])
|