Files
roboco/tests/unit/services/test_secretary_service.py
T

174 lines
6.1 KiB
Python
Raw Normal View History

"""roboco.services.secretary — directive gate + execution (mocked deps)."""
from __future__ import annotations
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from roboco.db.tables import SecretaryDirectiveTable
from roboco.models.secretary import DirectiveKind, DirectiveStatus
from roboco.services import secretary as sec_module
from roboco.services.base import ValidationError
from roboco.services.secretary import SecretaryService
def _session() -> MagicMock:
s = MagicMock()
s.add = MagicMock()
s.flush = AsyncMock()
return s
def _patch(monkeypatch: pytest.MonkeyPatch) -> dict[str, MagicMock]:
msg = MagicMock()
msg.post_to_channel = AsyncMock()
monkeypatch.setattr(sec_module, "get_messaging_service", lambda _s: msg)
goals = MagicMock()
goals.upsert = AsyncMock()
monkeypatch.setattr(sec_module, "get_company_goals_service", lambda _s: goals)
pitch = MagicMock()
pitch.approve = AsyncMock()
monkeypatch.setattr(sec_module, "get_pitch_service", lambda _s: pitch)
task = MagicMock()
task.approve_and_start = AsyncMock()
task.admin_set_status = AsyncMock()
monkeypatch.setattr(sec_module, "get_task_service", lambda _s: task)
notifier = MagicMock()
notifier.send_ack_notification = AsyncMock()
monkeypatch.setattr(
"roboco.services.notification.NotificationService", lambda: notifier
)
return {
"msg": msg,
"goals": goals,
"pitch": pitch,
"task": task,
"notifier": notifier,
}
def _pending(kind: DirectiveKind, payload: dict[str, Any]) -> SecretaryDirectiveTable:
return SecretaryDirectiveTable(
id=uuid4(),
kind=kind.value,
payload=payload,
status=DirectiveStatus.PENDING.value,
requested_by=uuid4(),
)
@pytest.mark.asyncio
async def test_relay_executes_directly(monkeypatch: pytest.MonkeyPatch) -> None:
svcs = _patch(monkeypatch)
svc = SecretaryService(_session())
row = await svc.submit_directive(
DirectiveKind.RELAY_MESSAGE,
{"channel": "all-hands", "text": "standup at 10"},
uuid4(),
)
assert row.status == DirectiveStatus.EXECUTED.value
svcs["msg"].post_to_channel.assert_awaited_once()
svcs["notifier"].send_ack_notification.assert_not_awaited()
@pytest.mark.asyncio
async def test_gated_charter_queues_and_notifies(
monkeypatch: pytest.MonkeyPatch,
) -> None:
svcs = _patch(monkeypatch)
svc = SecretaryService(_session())
row = await svc.submit_directive(
DirectiveKind.UPDATE_CHARTER, {"charter": {"north_star": "Win"}}, uuid4()
)
assert row.status == DirectiveStatus.PENDING.value
svcs["goals"].upsert.assert_not_awaited()
svcs["notifier"].send_ack_notification.assert_awaited_once()
@pytest.mark.asyncio
async def test_confirm_charter_executes(monkeypatch: pytest.MonkeyPatch) -> None:
svcs = _patch(monkeypatch)
svc = SecretaryService(_session())
row = _pending(DirectiveKind.UPDATE_CHARTER, {"charter": {"north_star": "Win"}})
monkeypatch.setattr(svc, "get_directive", AsyncMock(return_value=row))
out = await svc.confirm_directive(row.id, uuid4())
assert out.status == DirectiveStatus.EXECUTED.value
svcs["goals"].upsert.assert_awaited_once()
@pytest.mark.asyncio
async def test_confirm_control_task_start(monkeypatch: pytest.MonkeyPatch) -> None:
svcs = _patch(monkeypatch)
svc = SecretaryService(_session())
row = _pending(
DirectiveKind.CONTROL_TASK, {"task_id": str(uuid4()), "action": "start"}
)
monkeypatch.setattr(svc, "get_directive", AsyncMock(return_value=row))
out = await svc.confirm_directive(row.id, uuid4())
assert out.status == DirectiveStatus.EXECUTED.value
svcs["task"].approve_and_start.assert_awaited_once()
@pytest.mark.asyncio
async def test_confirm_approve_pitch(monkeypatch: pytest.MonkeyPatch) -> None:
svcs = _patch(monkeypatch)
svc = SecretaryService(_session())
row = _pending(DirectiveKind.APPROVE_PITCH, {"pitch_id": str(uuid4())})
monkeypatch.setattr(svc, "get_directive", AsyncMock(return_value=row))
out = await svc.confirm_directive(row.id, uuid4())
assert out.status == DirectiveStatus.EXECUTED.value
svcs["pitch"].approve.assert_awaited_once()
@pytest.mark.asyncio
async def test_announce_queues_then_confirm_posts(
monkeypatch: pytest.MonkeyPatch,
) -> None:
svcs = _patch(monkeypatch)
svc = SecretaryService(_session())
row = await svc.submit_directive(
DirectiveKind.ANNOUNCE, {"text": "we shipped v1"}, uuid4()
)
assert row.status == DirectiveStatus.PENDING.value
monkeypatch.setattr(svc, "get_directive", AsyncMock(return_value=row))
out = await svc.confirm_directive(row.id, uuid4())
assert out.status == DirectiveStatus.EXECUTED.value
svcs["msg"].post_to_channel.assert_awaited_once()
@pytest.mark.asyncio
async def test_reject_sets_rejected(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch)
svc = SecretaryService(_session())
row = _pending(DirectiveKind.ANNOUNCE, {"text": "x"})
monkeypatch.setattr(svc, "get_directive", AsyncMock(return_value=row))
out = await svc.reject_directive(row.id, uuid4(), "not now")
assert out.status == DirectiveStatus.REJECTED.value
assert out.result == "not now"
@pytest.mark.asyncio
async def test_missing_payload_raises(monkeypatch: pytest.MonkeyPatch) -> None:
_patch(monkeypatch)
svc = SecretaryService(_session())
with pytest.raises(ValidationError):
await svc.submit_directive(
DirectiveKind.RELAY_MESSAGE, {"channel": "x"}, uuid4()
)
@pytest.mark.asyncio
async def test_bad_task_action_fails_directive(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch(monkeypatch)
svc = SecretaryService(_session())
row = _pending(
DirectiveKind.CONTROL_TASK, {"task_id": str(uuid4()), "action": "explode"}
)
monkeypatch.setattr(svc, "get_directive", AsyncMock(return_value=row))
out = await svc.confirm_directive(row.id, uuid4())
assert out.status == DirectiveStatus.FAILED.value