mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
162 lines
5.1 KiB
Python
162 lines
5.1 KiB
Python
"""roboco.api.routes.secretary — role gates + directive flow (direct-call style)."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from http import HTTPStatus
|
||
|
|
from typing import Any
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from fastapi import HTTPException
|
||
|
|
from roboco.api.routes import secretary as sec_route
|
||
|
|
from roboco.api.schemas.secretary import DirectiveDecision, DirectiveSubmit
|
||
|
|
from roboco.models import AgentRole
|
||
|
|
from roboco.models.permissions import AgentContext
|
||
|
|
from roboco.services.base import ConflictError
|
||
|
|
|
||
|
|
_ROW = object()
|
||
|
|
_DIRECTIVE_DICT: dict[str, Any] = {
|
||
|
|
"id": "11111111-1111-1111-1111-111111111111",
|
||
|
|
"kind": "relay_message",
|
||
|
|
"status": "executed",
|
||
|
|
"payload": {},
|
||
|
|
"requested_by": "22222222-2222-2222-2222-222222222222",
|
||
|
|
"requested_at": None,
|
||
|
|
"decided_by": None,
|
||
|
|
"decided_at": None,
|
||
|
|
"result": "posted to #all-hands",
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def _agent(role: AgentRole) -> AgentContext:
|
||
|
|
return AgentContext(agent_id=uuid4(), role=role, team=None)
|
||
|
|
|
||
|
|
|
||
|
|
def _db() -> MagicMock:
|
||
|
|
db = MagicMock()
|
||
|
|
db.commit = AsyncMock()
|
||
|
|
return db
|
||
|
|
|
||
|
|
|
||
|
|
class _FakeService:
|
||
|
|
def __init__(self, *, exc: Exception | None = None) -> None:
|
||
|
|
self._exc = exc
|
||
|
|
|
||
|
|
async def submit_directive(self, _kind: Any, _payload: Any, _by: Any) -> object:
|
||
|
|
if self._exc is not None:
|
||
|
|
raise self._exc
|
||
|
|
return _ROW
|
||
|
|
|
||
|
|
async def confirm_directive(self, _directive_id: Any, _by: Any) -> object:
|
||
|
|
if self._exc is not None:
|
||
|
|
raise self._exc
|
||
|
|
return _ROW
|
||
|
|
|
||
|
|
async def reject_directive(
|
||
|
|
self, _directive_id: Any, _by: Any, _reason: Any
|
||
|
|
) -> object:
|
||
|
|
if self._exc is not None:
|
||
|
|
raise self._exc
|
||
|
|
return _ROW
|
||
|
|
|
||
|
|
async def list_directives(self, _status: Any = None) -> list[object]:
|
||
|
|
return [_ROW]
|
||
|
|
|
||
|
|
async def read_company_state(self) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"goals": {},
|
||
|
|
"task_counts": {},
|
||
|
|
"pending_pitches": [],
|
||
|
|
"pending_directives": [],
|
||
|
|
}
|
||
|
|
|
||
|
|
def to_dict(self, _row: object) -> dict[str, Any]:
|
||
|
|
return dict(_DIRECTIVE_DICT)
|
||
|
|
|
||
|
|
|
||
|
|
def _install(monkeypatch: pytest.MonkeyPatch, service: _FakeService) -> None:
|
||
|
|
monkeypatch.setattr(sec_route, "get_secretary_service", lambda _db: service)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_submit_forbidden_for_developer() -> None:
|
||
|
|
with pytest.raises(HTTPException) as exc:
|
||
|
|
await sec_route.submit_directive(
|
||
|
|
DirectiveSubmit(kind="relay_message"), _db(), _agent(AgentRole.DEVELOPER)
|
||
|
|
)
|
||
|
|
assert exc.value.status_code == HTTPStatus.FORBIDDEN
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_submit_bad_kind_422() -> None:
|
||
|
|
with pytest.raises(HTTPException) as exc:
|
||
|
|
await sec_route.submit_directive(
|
||
|
|
DirectiveSubmit(kind="bogus"), _db(), _agent(AgentRole.SECRETARY)
|
||
|
|
)
|
||
|
|
assert exc.value.status_code == HTTPStatus.UNPROCESSABLE_ENTITY
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_submit_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
db = _db()
|
||
|
|
_install(monkeypatch, _FakeService())
|
||
|
|
resp = await sec_route.submit_directive(
|
||
|
|
DirectiveSubmit(
|
||
|
|
kind="relay_message", payload={"channel": "all-hands", "text": "hi"}
|
||
|
|
),
|
||
|
|
db,
|
||
|
|
_agent(AgentRole.SECRETARY),
|
||
|
|
)
|
||
|
|
assert resp.status == "executed"
|
||
|
|
db.commit.assert_awaited_once()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_confirm_forbidden_for_secretary() -> None:
|
||
|
|
with pytest.raises(HTTPException) as exc:
|
||
|
|
await sec_route.confirm_directive(uuid4(), _db(), _agent(AgentRole.SECRETARY))
|
||
|
|
assert exc.value.status_code == HTTPStatus.FORBIDDEN
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_confirm_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
db = _db()
|
||
|
|
_install(monkeypatch, _FakeService())
|
||
|
|
resp = await sec_route.confirm_directive(uuid4(), db, _agent(AgentRole.CEO))
|
||
|
|
assert resp.id == _DIRECTIVE_DICT["id"]
|
||
|
|
db.commit.assert_awaited_once()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_confirm_conflict_409(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
_install(monkeypatch, _FakeService(exc=ConflictError("already decided")))
|
||
|
|
with pytest.raises(HTTPException) as exc:
|
||
|
|
await sec_route.confirm_directive(uuid4(), _db(), _agent(AgentRole.CEO))
|
||
|
|
assert exc.value.status_code == HTTPStatus.CONFLICT
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_list_forbidden_for_secretary() -> None:
|
||
|
|
with pytest.raises(HTTPException) as exc:
|
||
|
|
await sec_route.list_directives(_db(), _agent(AgentRole.SECRETARY))
|
||
|
|
assert exc.value.status_code == HTTPStatus.FORBIDDEN
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_reject_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
db = _db()
|
||
|
|
_install(monkeypatch, _FakeService())
|
||
|
|
resp = await sec_route.reject_directive(
|
||
|
|
uuid4(), DirectiveDecision(reason="no"), db, _agent(AgentRole.CEO)
|
||
|
|
)
|
||
|
|
assert resp.id == _DIRECTIVE_DICT["id"]
|
||
|
|
db.commit.assert_awaited_once()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_state_allows_secretary(monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
_install(monkeypatch, _FakeService())
|
||
|
|
resp = await sec_route.read_state(_db(), _agent(AgentRole.SECRETARY))
|
||
|
|
assert resp.pending_pitches == []
|