Files
roboco/tests/unit/services/test_supersede_umbrella.py
T

151 lines
5.5 KiB
Python

"""Supersede umbrella close-on-land guards (orchestration-marker storage).
The supersede marker (``pr={n} review={uuid}`` plus a ``closed=1`` token once
the contributor PR is retired) lives in ``orchestration_markers`` after
migration 041 — isolated from the human ``quick_context``, so CEO escalation /
approval notes can no longer be mistaken for it.
"""
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from roboco.models.base import TaskStatus
from roboco.services.task import TaskService, supersede_marker_line
_VALUE = "pr=5 review=abc"
def _scalars_all(rows: list[object]) -> MagicMock:
res = MagicMock()
res.scalars.return_value.all.return_value = rows
return res
def _service(execute_returns: object) -> TaskService:
session = MagicMock()
session.execute = AsyncMock(return_value=execute_returns)
session.flush = AsyncMock()
return TaskService(session)
def _bind(svc: TaskService, name: str, value: object) -> None:
object.__setattr__(svc, name, value)
def _task(supersede: str | None = None, **kw: Any) -> SimpleNamespace:
om = {"external_pr_supersede": supersede} if supersede is not None else None
return SimpleNamespace(orchestration_markers=om, **kw)
# ---------------------------------------------------------------------------
# supersede_marker_line — reads the marker value
# ---------------------------------------------------------------------------
def test_marker_line_returns_value() -> None:
assert supersede_marker_line(_task(_VALUE)) == _VALUE
def test_marker_line_empty_when_absent() -> None:
assert supersede_marker_line(_task()) == ""
assert supersede_marker_line(SimpleNamespace(orchestration_markers=None)) == ""
# ---------------------------------------------------------------------------
# supersede_umbrellas_pending_close — closed-token + landed-replacement gates
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_pending_close_excludes_umbrella_with_closed_marker() -> None:
umbrella = _task(f"{_VALUE} closed=1", id=uuid4())
svc = _service(_scalars_all([umbrella]))
_bind(svc, "_supersede_replacement_landed", AsyncMock(return_value=True))
assert await svc.supersede_umbrellas_pending_close() == []
@pytest.mark.asyncio
async def test_pending_close_keeps_open_landed_umbrella() -> None:
umbrella = _task(_VALUE, id=uuid4())
svc = _service(_scalars_all([umbrella]))
_bind(svc, "_supersede_replacement_landed", AsyncMock(return_value=True))
assert await svc.supersede_umbrellas_pending_close() == [umbrella]
@pytest.mark.asyncio
async def test_pending_close_requires_landed_replacement() -> None:
# COMPLETED + no closed marker, but the replacement never landed (the code
# subtask was cancelled) — close-on-land must skip it.
umbrella = _task(_VALUE, id=uuid4())
svc = _service(_scalars_all([umbrella]))
_bind(svc, "_supersede_replacement_landed", AsyncMock(return_value=False))
assert await svc.supersede_umbrellas_pending_close() == []
# ---------------------------------------------------------------------------
# _supersede_replacement_landed — subtree walk
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_replacement_landed_true_for_completed_descendant_with_pr() -> None:
child = MagicMock(id=uuid4(), status=TaskStatus.COMPLETED, pr_number=42)
svc = _service(_scalars_all([child]))
assert await svc._supersede_replacement_landed(uuid4()) is True
@pytest.mark.asyncio
async def test_replacement_landed_false_when_descendant_cancelled() -> None:
child = MagicMock(id=uuid4(), status=TaskStatus.CANCELLED, pr_number=42)
svc = _service(_scalars_all([child]))
assert await svc._supersede_replacement_landed(uuid4()) is False
@pytest.mark.asyncio
async def test_replacement_landed_false_when_completed_without_pr() -> None:
child = MagicMock(id=uuid4(), status=TaskStatus.COMPLETED, pr_number=None)
svc = _service(_scalars_all([child]))
assert await svc._supersede_replacement_landed(uuid4()) is False
# ---------------------------------------------------------------------------
# find_supersede_umbrella — value dedup (pr=N review= prefix match)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_find_umbrella_matches_by_value() -> None:
match = _task(_VALUE, id=uuid4())
other = _task("pr=9 review=z", id=uuid4()) # different PR
svc = _service(_scalars_all([other, match]))
found = await svc.find_supersede_umbrella(uuid4(), 5)
assert found is match
# ---------------------------------------------------------------------------
# mark_supersede_pr_closed — token appended to the marker value
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_mark_closed_appends_token() -> None:
task = _task(_VALUE)
svc = _service(_scalars_all([]))
_bind(svc, "get", AsyncMock(return_value=task))
await svc.mark_supersede_pr_closed(uuid4())
assert supersede_marker_line(task) == f"{_VALUE} closed=1"
@pytest.mark.asyncio
async def test_mark_closed_is_idempotent() -> None:
task = _task(f"{_VALUE} closed=1")
svc = _service(_scalars_all([]))
_bind(svc, "get", AsyncMock(return_value=task))
await svc.mark_supersede_pr_closed(uuid4())
assert supersede_marker_line(task) == f"{_VALUE} closed=1"