fix(ceo-approve): require substantive notes; panel collects them

ceo-approve was bound to QANotes (notes required), so the panel's
one-click approve (posts {}) 422'd. The wrong fix is to waive notes —
that empties the audit record for a production merge. Instead require
substantive notes (>=20 chars, mirroring pass-qa) and make the panel
COLLECT them: a new CeoApproveDialog (mirrors the reject dialog) gates
the 'Approve & Merge' action, and the dashboard approval-queue enforces
the same before POSTing. The CEO sign-off note is now always captured.
This commit is contained in:
Renn F
2026-05-24 06:59:37 +02:00
parent d49d1cdb37
commit c093996efc
5 changed files with 133 additions and 8 deletions
+21 -2
View File
@@ -2434,7 +2434,7 @@ async def test_ceo_approve_service_returns_none(ceo_client: dict) -> None:
await ceo_client["db"].flush()
response = await ceo_client["client"].post(
f"/api/tasks/{task.id}/ceo-approve",
json={"notes": "approved"},
json={"notes": "Reviewed and approved for production release."},
headers=_HDR,
)
assert response.status_code == HTTPStatus.BAD_REQUEST
@@ -2452,12 +2452,31 @@ async def test_ceo_approve_success(ceo_client: dict) -> None:
mock_factory.return_value = instance
response = await ceo_client["client"].post(
f"/api/tasks/{task.id}/ceo-approve",
json={"notes": "approved"},
json={"notes": "Verified against all acceptance criteria; approved."},
headers=_HDR,
)
assert response.status_code == HTTPStatus.OK
@pytest.mark.asyncio
async def test_ceo_approve_without_notes_rejected(ceo_client: dict) -> None:
"""Audit: a CEO approval with no/thin notes leaves no record of WHY the
work shipped, so the endpoint must reject it (>= 20 chars required). The
panel collects the note before POSTing."""
task = _seed_task_ceo(ceo_client)
await ceo_client["db"].flush()
for body in ({}, {"notes": ""}, {"notes": "lgtm"}):
response = await ceo_client["client"].post(
f"/api/tasks/{task.id}/ceo-approve",
json=body,
headers=_HDR,
)
assert response.status_code in (
HTTPStatus.BAD_REQUEST,
HTTPStatus.UNPROCESSABLE_ENTITY,
), (body, response.status_code)
@pytest.mark.asyncio
async def test_ceo_reject_task_not_found(ceo_client: dict) -> None:
response = await ceo_client["client"].post(