mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
151 lines
5.5 KiB
Python
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"
|