mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
100% Coverage
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""Health route coverage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import FastAPI
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from roboco.api.routes.health import router as health_router
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def health_client() -> AsyncIterator[AsyncClient]:
|
||||
app = FastAPI()
|
||||
app.include_router(health_router)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_returns_ok(health_client: AsyncClient) -> None:
|
||||
response = await health_client.get("/health")
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
body = response.json()
|
||||
assert body["status"] == "ok"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readiness_check_all_healthy(health_client: AsyncClient) -> None:
|
||||
"""When DB + Redis are healthy → status: ok (line 41-46)."""
|
||||
with (
|
||||
patch(
|
||||
"roboco.api.routes.health.check_database",
|
||||
AsyncMock(return_value=("connected", True)),
|
||||
),
|
||||
patch(
|
||||
"roboco.api.routes.health.check_redis",
|
||||
AsyncMock(return_value=("connected", True)),
|
||||
),
|
||||
):
|
||||
response = await health_client.get("/ready")
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
body = response.json()
|
||||
assert body["status"] == "ok"
|
||||
assert body["database"] == "connected"
|
||||
assert body["redis"] == "connected"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readiness_check_degraded(health_client: AsyncClient) -> None:
|
||||
"""When DB is down → status: degraded."""
|
||||
with (
|
||||
patch(
|
||||
"roboco.api.routes.health.check_database",
|
||||
AsyncMock(return_value=("disconnected", False)),
|
||||
),
|
||||
patch(
|
||||
"roboco.api.routes.health.check_redis",
|
||||
AsyncMock(return_value=("connected", True)),
|
||||
),
|
||||
):
|
||||
response = await health_client.get("/ready")
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
body = response.json()
|
||||
assert body["status"] == "degraded"
|
||||
@@ -213,3 +213,23 @@ async def test_evidence_with_task_id_returns_evidence_envelope() -> None:
|
||||
assert body["evidence"]["commits"] == ["abc123"]
|
||||
mock_actions.evidence.assert_awaited_once()
|
||||
assert str(mock_actions.evidence.call_args.kwargs["task_id"]) == _TASK_ID
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_notify_dispatches_target_text_priority() -> None:
|
||||
"""POST /api/v2/do/notify forwards target/text/priority to ContentActions."""
|
||||
mock_actions = MagicMock(spec=ContentActions)
|
||||
mock_actions.notify = AsyncMock(
|
||||
return_value=_make_envelope(status="ok", task_id=None)
|
||||
)
|
||||
client = TestClient(_build_app(mock_actions))
|
||||
resp = client.post(
|
||||
"/api/v2/do/notify",
|
||||
json={"target": "be-pm", "text": "ack me", "priority": "normal"},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_actions.notify.assert_awaited_once()
|
||||
call_kwargs = mock_actions.notify.call_args.kwargs
|
||||
assert call_kwargs["target"] == "be-pm"
|
||||
assert call_kwargs["text"] == "ack me"
|
||||
|
||||
@@ -300,3 +300,35 @@ def test_submit_up_rejects_empty_notes() -> None:
|
||||
)
|
||||
|
||||
assert resp.status_code == _HTTP_422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unclaim_dispatches() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.unclaim = AsyncMock(
|
||||
return_value=_make_envelope(status="awaiting_pm_review", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/cell_pm/unclaim",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.unclaim.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_dispatches() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.resume = AsyncMock(
|
||||
return_value=_make_envelope(status="in_progress", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/cell_pm/resume",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.resume.assert_awaited_once()
|
||||
|
||||
@@ -154,3 +154,54 @@ def test_i_am_blocked_rejects_empty_reason() -> None:
|
||||
)
|
||||
|
||||
assert resp.status_code == _HTTP_422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submit_for_qa_dispatches_task_id() -> None:
|
||||
"""POST submit_for_qa forwards task_id."""
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.submit_for_qa = AsyncMock(
|
||||
return_value=_make_envelope(status="awaiting_qa", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/developer/submit_for_qa",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.submit_for_qa.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unclaim_dispatches_task_id() -> None:
|
||||
"""POST unclaim forwards task_id."""
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.unclaim = AsyncMock(
|
||||
return_value=_make_envelope(status="pending", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/developer/unclaim",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.unclaim.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_dispatches_task_id() -> None:
|
||||
"""POST resume forwards task_id."""
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.resume = AsyncMock(
|
||||
return_value=_make_envelope(status="in_progress", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/developer/resume",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.resume.assert_awaited_once()
|
||||
|
||||
@@ -158,3 +158,35 @@ def test_i_documented_rejects_empty_files_list() -> None:
|
||||
)
|
||||
|
||||
assert resp.status_code == _HTTP_422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unclaim_dispatches() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.unclaim = AsyncMock(
|
||||
return_value=_make_envelope(status="awaiting_documentation", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/documenter/unclaim",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.unclaim.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_dispatches() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.resume = AsyncMock(
|
||||
return_value=_make_envelope(status="claimed", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/documenter/resume",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.resume.assert_awaited_once()
|
||||
|
||||
@@ -234,4 +234,51 @@ async def test_delegate_to_cell_pm_dispatches_inputs_bundle() -> None:
|
||||
)
|
||||
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.delegate.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_escalate_to_ceo_dispatches() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.escalate_to_ceo = AsyncMock(
|
||||
return_value=_make_envelope(status="awaiting_ceo_approval", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/main_pm/escalate_to_ceo",
|
||||
json={"task_id": _TASK_ID, "reason": "needs CEO sign-off"},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.escalate_to_ceo.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unclaim_dispatches() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.unclaim = AsyncMock(
|
||||
return_value=_make_envelope(status="awaiting_pm_review", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/main_pm/unclaim",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.unclaim.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_dispatches() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.resume = AsyncMock(
|
||||
return_value=_make_envelope(status="in_progress", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/main_pm/resume",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.resume.assert_awaited_once()
|
||||
|
||||
@@ -173,3 +173,49 @@ def test_fail_review_rejects_empty_issues_list() -> None:
|
||||
)
|
||||
|
||||
assert resp.status_code == _HTTP_422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unclaim_dispatches_task_id() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.unclaim = AsyncMock(
|
||||
return_value=_make_envelope(status="awaiting_qa", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/qa/unclaim",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.unclaim.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resume_dispatches_task_id() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.resume = AsyncMock(
|
||||
return_value=_make_envelope(status="claimed", task_id=_TASK_ID)
|
||||
)
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/qa/resume",
|
||||
json={"task_id": _TASK_ID},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.resume.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_i_am_idle_dispatches_agent_id() -> None:
|
||||
mock_chore = MagicMock()
|
||||
mock_chore.i_am_idle = AsyncMock(return_value=_make_envelope(status="idle"))
|
||||
client = TestClient(_build_app(mock_chore))
|
||||
resp = client.post(
|
||||
"/api/v2/flow/qa/i_am_idle",
|
||||
json={},
|
||||
headers=_HEADERS,
|
||||
)
|
||||
assert resp.status_code == _HTTP_200
|
||||
mock_chore.i_am_idle.assert_awaited_once()
|
||||
|
||||
Reference in New Issue
Block a user