mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(coroner): playbook-kind postmortems stop rendering dead buttons
A kind=playbook process change drafts into the playbook queue at propose time and both approve/reject refuse it - but legacy rows carry no marker status, so the response defaulted to proposed and the panel rendered approve/dismiss buttons that bounce forever. The list route now derives not_applicable for playbook-kind changes regardless of stored status, which the panel already renders as its Drafted-as-playbook badge.
This commit is contained in:
@@ -25,7 +25,7 @@ from roboco.api.schemas.coroner import (
|
|||||||
)
|
)
|
||||||
from roboco.foundation.policy.content import markers
|
from roboco.foundation.policy.content import markers
|
||||||
from roboco.security import guard_deco
|
from roboco.security import guard_deco
|
||||||
from roboco.services.coroner_service import get_coroner_service
|
from roboco.services.coroner_service import PLAYBOOK_KIND, get_coroner_service
|
||||||
from roboco.services.task import get_task_service
|
from roboco.services.task import get_task_service
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -55,7 +55,16 @@ def _to_response(task: TaskTable) -> PostmortemResponse:
|
|||||||
process_change_kind=process_change.get("kind"),
|
process_change_kind=process_change.get("kind"),
|
||||||
process_change_description=process_change.get("description"),
|
process_change_description=process_change.get("description"),
|
||||||
playbook_id=postmortem.get("playbook_id"),
|
playbook_id=postmortem.get("playbook_id"),
|
||||||
process_change_status=process_change.get("status", "proposed"),
|
# A playbook-kind change already drafted into the playbook queue at
|
||||||
|
# propose time — there is nothing to decide, but the stored status
|
||||||
|
# stays "proposed", which left the panel rendering approve/dismiss
|
||||||
|
# buttons that both verbs refuse forever. Derive the terminal status
|
||||||
|
# the panel's contract expects instead.
|
||||||
|
process_change_status=(
|
||||||
|
"not_applicable"
|
||||||
|
if process_change.get("kind") == PLAYBOOK_KIND
|
||||||
|
else process_change.get("status", "proposed")
|
||||||
|
),
|
||||||
process_change_reject_reason=process_change.get("reject_reason"),
|
process_change_reject_reason=process_change.get("reject_reason"),
|
||||||
process_change_materialized_task_id=process_change.get("materialized_task_id"),
|
process_change_materialized_task_id=process_change.get("materialized_task_id"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from roboco.db.tables import TaskTable
|
from roboco.db.tables import TaskTable
|
||||||
|
|
||||||
_PLAYBOOK_KIND = "playbook"
|
PLAYBOOK_KIND = "playbook"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -90,7 +90,7 @@ class CoronerService(BaseService):
|
|||||||
task, payload, process_change = await self._find(task_id)
|
task, payload, process_change = await self._find(task_id)
|
||||||
if task is None or payload is None or process_change is None:
|
if task is None or payload is None or process_change is None:
|
||||||
return None
|
return None
|
||||||
if process_change["kind"] == _PLAYBOOK_KIND:
|
if process_change["kind"] == PLAYBOOK_KIND:
|
||||||
return self._playbook_result()
|
return self._playbook_result()
|
||||||
if process_change["status"] == "approved":
|
if process_change["status"] == "approved":
|
||||||
return ProcessChangeResult(
|
return ProcessChangeResult(
|
||||||
@@ -139,7 +139,7 @@ class CoronerService(BaseService):
|
|||||||
task, payload, process_change = await self._find(task_id)
|
task, payload, process_change = await self._find(task_id)
|
||||||
if task is None or payload is None or process_change is None:
|
if task is None or payload is None or process_change is None:
|
||||||
return None
|
return None
|
||||||
if process_change["kind"] == _PLAYBOOK_KIND:
|
if process_change["kind"] == PLAYBOOK_KIND:
|
||||||
return self._playbook_result()
|
return self._playbook_result()
|
||||||
if process_change["status"] == "rejected":
|
if process_change["status"] == "rejected":
|
||||||
return ProcessChangeResult(
|
return ProcessChangeResult(
|
||||||
|
|||||||
@@ -200,6 +200,28 @@ async def test_list_postmortems_returns_completed_autopsy(
|
|||||||
assert row["failed_stage"] == "awaiting_qa"
|
assert row["failed_stage"] == "awaiting_qa"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_list_postmortems_derives_not_applicable_for_playbook_kind(
|
||||||
|
db_session: AsyncSession, ceo_client: AsyncClient
|
||||||
|
) -> None:
|
||||||
|
"""A playbook-kind process change reads as not_applicable even when the
|
||||||
|
stored marker has NO status key — legacy rows predate the propose-time
|
||||||
|
stamp, and the raw default ("proposed") left the panel rendering
|
||||||
|
approve/dismiss buttons both verbs refuse forever."""
|
||||||
|
task = await _seed_completed_postmortem(db_session)
|
||||||
|
payload = markers.get_coroner_postmortem(task)
|
||||||
|
assert payload is not None
|
||||||
|
payload["process_change"] = {
|
||||||
|
"kind": "playbook",
|
||||||
|
"description": "run the named command end-to-end",
|
||||||
|
}
|
||||||
|
markers.set_coroner_postmortem(task, payload)
|
||||||
|
await db_session.flush()
|
||||||
|
resp = await ceo_client.get("/api/coroner/postmortems")
|
||||||
|
assert resp.status_code == HTTPStatus.OK
|
||||||
|
assert resp.json()[0]["process_change_status"] == "not_applicable"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_list_postmortems_forbidden_for_non_ceo(
|
async def test_list_postmortems_forbidden_for_non_ceo(
|
||||||
dev_client: AsyncClient,
|
dev_client: AsyncClient,
|
||||||
|
|||||||
Reference in New Issue
Block a user