"""Groups API route coverage.""" from __future__ import annotations import uuid from http import HTTPStatus from typing import TYPE_CHECKING, cast from unittest.mock import patch from uuid import uuid4 import pytest import pytest_asyncio from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from roboco.api.deps import get_agent_context, get_db from roboco.api.routes.groups import router as groups_router from roboco.db.tables import AgentTable, ChannelTable, GroupTable from roboco.models import AgentRole, AgentStatus, Team from roboco.models.base import ChannelType from roboco.models.permissions import AgentContext if TYPE_CHECKING: from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @pytest_asyncio.fixture async def groups_client( db_session: AsyncSession, ) -> AsyncIterator[dict]: pm = AgentTable( id=uuid4(), name="MainPM", slug=f"main-pm-{uuid4().hex[:8]}", role=AgentRole.MAIN_PM, team=None, status=AgentStatus.ACTIVE, model_config={}, system_prompt="pm", capabilities=[], permissions={}, metrics={}, ) db_session.add(pm) await db_session.flush() channel = ChannelTable( id=uuid4(), name="ch", slug=f"ch-{uuid4().hex[:6]}", type=ChannelType.CELL, ) db_session.add(channel) await db_session.flush() app = FastAPI() app.include_router(groups_router, prefix="/api/groups") async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( agent_id=cast("uuid.UUID", pm.id), role=AgentRole.MAIN_PM, team=None ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: yield {"client": client, "channel": channel, "pm": pm} app.dependency_overrides.clear() _HDR = {"X-Agent-ID": str(uuid4()), "X-Agent-Role": "main_pm"} @pytest.mark.asyncio async def test_create_group_main_pm(groups_client: dict) -> None: client = groups_client["client"] response = await client.post( "/api/groups", json={ "channel_slug": groups_client["channel"].slug, "name": "Sprint 1", "hierarchy_level": 4, "allowed_roles": [], }, headers=_HDR, ) assert response.status_code == HTTPStatus.CREATED @pytest.mark.asyncio async def test_create_group_unknown_channel(groups_client: dict) -> None: client = groups_client["client"] response = await client.post( "/api/groups", json={ "channel_slug": "ghost-channel", "name": "Sprint 1", "hierarchy_level": 4, }, headers=_HDR, ) assert response.status_code == HTTPStatus.NOT_FOUND @pytest.mark.asyncio async def test_create_group_value_error_returns_400(groups_client: dict) -> None: """Lines 84-88: ValueError from service.create_group → 400.""" client = groups_client["client"] with patch( "roboco.services.messaging.MessagingService.create_group", side_effect=ValueError("invalid"), ): response = await client.post( "/api/groups", json={ "channel_slug": groups_client["channel"].slug, "name": "Sprint 1", "hierarchy_level": 4, "allowed_roles": [], }, headers=_HDR, ) assert response.status_code == HTTPStatus.BAD_REQUEST @pytest.mark.asyncio async def test_get_group_not_found(groups_client: dict) -> None: client = groups_client["client"] response = await client.get(f"/api/groups/{uuid4()}", headers=_HDR) assert response.status_code == HTTPStatus.NOT_FOUND @pytest.mark.asyncio async def test_get_group(groups_client: dict, db_session: AsyncSession) -> None: client = groups_client["client"] group = GroupTable( id=uuid4(), name="g1", channel_id=groups_client["channel"].id, hierarchy_level=4, ) db_session.add(group) await db_session.flush() response = await client.get(f"/api/groups/{group.id}", headers=_HDR) assert response.status_code == HTTPStatus.OK @pytest.mark.asyncio async def test_create_group_developer_forbidden( db_session: AsyncSession, ) -> None: """Developers can't create groups.""" dev = AgentTable( id=uuid4(), name="Dev", slug=f"be-dev-{uuid4().hex[:8]}", role=AgentRole.DEVELOPER, team=Team.BACKEND, status=AgentStatus.ACTIVE, model_config={}, system_prompt="dev", capabilities=[], permissions={}, metrics={}, ) db_session.add(dev) await db_session.flush() app = FastAPI() app.include_router(groups_router, prefix="/api/groups") async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( agent_id=cast("uuid.UUID", dev.id), role=AgentRole.DEVELOPER, team=Team.BACKEND, ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post( "/api/groups", json={ "channel_slug": "backend-cell", "name": "x", "hierarchy_level": 4, }, headers=_HDR, ) app.dependency_overrides.clear() assert response.status_code == HTTPStatus.FORBIDDEN