mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
refactor(content): move orchestration markers off quick_context to typed jsonb
This commit is contained in:
@@ -1,28 +1,50 @@
|
||||
"""External-PR review dedup — review once per (project, PR, head commit).
|
||||
|
||||
``external_review_task_exists`` drives re-review off the PR's head SHA: an
|
||||
unchanged PR (same head) is skipped, new commits (a new head SHA) open a fresh
|
||||
review, and legacy/unknown-SHA tasks are never re-reviewed (no spam).
|
||||
``external_review_task_exists`` drives re-review off the PR's head SHA, stored as
|
||||
the ``external_pr_head`` orchestration marker (migration 041); dismissal is the
|
||||
``dismissed`` marker. An unchanged PR (same head) is skipped, new commits open a
|
||||
fresh review, and legacy/unknown-SHA tasks are never re-reviewed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from roboco.foundation.policy.content import markers
|
||||
from roboco.services.task import TaskService
|
||||
|
||||
|
||||
def _service(quick_contexts: list[str | None]) -> TaskService:
|
||||
"""A TaskService whose review-task query returns these quick_context values."""
|
||||
def _service(scalar_rows: list[object]) -> TaskService:
|
||||
"""A TaskService whose next query returns these scalar rows."""
|
||||
res = MagicMock()
|
||||
res.scalars.return_value.all.return_value = quick_contexts
|
||||
res.scalars.return_value.all.return_value = scalar_rows
|
||||
session = MagicMock()
|
||||
session.execute = AsyncMock(return_value=res)
|
||||
session.flush = AsyncMock()
|
||||
return TaskService(session)
|
||||
|
||||
|
||||
def _markers(head: str | None = None, dismissed: bool = False) -> dict:
|
||||
om: dict = {}
|
||||
if head is not None:
|
||||
om["external_pr_head"] = head
|
||||
if dismissed:
|
||||
om["dismissed"] = True
|
||||
return om
|
||||
|
||||
|
||||
def _bind(svc: TaskService, name: str, value: object) -> None:
|
||||
object.__setattr__(svc, name, value)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# external_review_task_exists — scalars are orchestration_markers dicts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_task_yet_ingests() -> None:
|
||||
svc = _service([])
|
||||
@@ -31,14 +53,14 @@ async def test_no_task_yet_ingests() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_same_head_sha_skips() -> None:
|
||||
svc = _service(["external_pr_head=abc"])
|
||||
svc = _service([_markers("abc")])
|
||||
assert await svc.external_review_task_exists(uuid4(), 170, "abc") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_new_head_sha_rereviews() -> None:
|
||||
# PR got new commits since the last review → open a fresh review.
|
||||
svc = _service(["external_pr_head=abc"])
|
||||
svc = _service([_markers("abc")])
|
||||
assert await svc.external_review_task_exists(uuid4(), 170, "def") is False
|
||||
|
||||
|
||||
@@ -52,25 +74,26 @@ async def test_legacy_markerless_task_not_rereviewed() -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_head_sha_does_not_spam() -> None:
|
||||
# Can't detect change (no SHA from GitHub) → treat as reviewed.
|
||||
svc = _service(["external_pr_head=abc"])
|
||||
svc = _service([_markers("abc")])
|
||||
assert await svc.external_review_task_exists(uuid4(), 170, None) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_old_shas_still_rereviews_new() -> None:
|
||||
svc = _service(["external_pr_head=abc", "external_pr_head=def"])
|
||||
svc = _service([_markers("abc"), _markers("def")])
|
||||
assert await svc.external_review_task_exists(uuid4(), 170, "ghi") is False
|
||||
assert await svc.external_review_task_exists(uuid4(), 170, "def") is True
|
||||
|
||||
|
||||
def _bind(svc: TaskService, name: str, value: object) -> None:
|
||||
object.__setattr__(svc, name, value)
|
||||
# ---------------------------------------------------------------------------
|
||||
# list queues — post-query dismissed filter (scalars are task objects)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_awaiting_decision_excludes_dismissed() -> None:
|
||||
pending = MagicMock(quick_context="external_pr_head=abc")
|
||||
dismissed = MagicMock(quick_context="external_pr_head=def dismissed=1")
|
||||
pending = SimpleNamespace(orchestration_markers=_markers("abc"))
|
||||
dismissed = SimpleNamespace(orchestration_markers=_markers("def", dismissed=True))
|
||||
svc = _service([pending, dismissed])
|
||||
out = await svc.list_external_pr_reviews_awaiting_decision()
|
||||
assert out == [pending]
|
||||
@@ -78,10 +101,8 @@ async def test_list_awaiting_decision_excludes_dismissed() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_external_pr_reviews_excludes_dismissed() -> None:
|
||||
# The panel queue surfaces in-flight reviews too (the status filter lives in
|
||||
# SQL); here we pin the post-query behavior: dismissed reviews drop out.
|
||||
reviewing = MagicMock(quick_context="external_pr_head=abc")
|
||||
dismissed = MagicMock(quick_context="external_pr_head=def dismissed=1")
|
||||
reviewing = SimpleNamespace(orchestration_markers=_markers("abc"))
|
||||
dismissed = SimpleNamespace(orchestration_markers=_markers("def", dismissed=True))
|
||||
svc = _service([reviewing, dismissed])
|
||||
out = await svc.list_external_pr_reviews()
|
||||
assert out == [reviewing]
|
||||
@@ -89,15 +110,14 @@ async def test_list_external_pr_reviews_excludes_dismissed() -> None:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dismiss_marks_and_is_idempotent() -> None:
|
||||
task = MagicMock(source="external_pr", quick_context="external_pr_head=abc")
|
||||
session = MagicMock()
|
||||
session.flush = AsyncMock()
|
||||
svc = TaskService(session)
|
||||
task = SimpleNamespace(source="external_pr", orchestration_markers=_markers("abc"))
|
||||
svc = _service([])
|
||||
_bind(svc, "get", AsyncMock(return_value=task))
|
||||
await svc.dismiss_external_pr_review(uuid4())
|
||||
assert "dismissed=1" in task.quick_context.split()
|
||||
assert markers.is_dismissed(task) is True
|
||||
await svc.dismiss_external_pr_review(uuid4()) # idempotent
|
||||
assert task.quick_context.split().count("dismissed=1") == 1
|
||||
assert markers.is_dismissed(task) is True
|
||||
assert task.orchestration_markers["dismissed"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -111,7 +131,7 @@ async def test_dismiss_rejects_non_external_pr() -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# active_task_owns_branch — the internal-PR "is this a lifecycle PR?" check (#3)
|
||||
# active_task_owns_branch — the internal-PR "is this a lifecycle PR?" check
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user