mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Service-level tests now exercise provider, permissions, project, journal, messaging, work_session, metrics, kanban, extraction, learning, notification, dashboard, llm_routing, a2a, task, repository_base, audit, db_seed, branch_name, indexed_document, query_helpers, agent. API route tests cover provider, journal, project, sessions, dashboard, work_session, tasks, a2a, groups, notifications, agents, channels, messages, kanban, api_resources. Pure-function helpers covered: handlers, deps_helpers, middleware, middleware_docs, transcription, pr templates, agents_config, errors, logging, journal/notification/channel/a2a access, task_lifecycle, streaming, converters, crypto, schemas (common + websocket), events, permissions extras. pyproject ruff per-file-ignores extended for tests so PLR2004 (status code magic values), PLC0415 (lazy imports), PLR0913 (fixture params), ARG001 (unused fixture deps), SIM105, and E501 don't fight test idioms.
341 lines
12 KiB
Python
341 lines
12 KiB
Python
"""PermissionService coverage — RBAC for channels, notifications, tasks, KB.
|
|
|
|
Pure-logic checks driven by ``agents_config`` constants — no DB needed.
|
|
The service is a SingletonService, so we instantiate it directly with
|
|
``object.__new__`` to bypass session-management.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from roboco.models import AgentRole, Team
|
|
from roboco.models.permissions import (
|
|
AgentContext,
|
|
KBAction,
|
|
PermissionLevel,
|
|
TaskAction,
|
|
)
|
|
from roboco.services.permissions import PermissionService
|
|
|
|
|
|
@pytest.fixture
|
|
def svc() -> PermissionService:
|
|
"""PermissionService is a SingletonService — bypass __init__ for unit tests."""
|
|
return object.__new__(PermissionService)
|
|
|
|
|
|
def _ctx(role: AgentRole, team: Team | None = None) -> AgentContext:
|
|
return AgentContext(agent_id=uuid4(), role=role, team=team)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Channel read access
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_auditor_can_read_any_channel(svc: PermissionService) -> None:
|
|
"""AUDITOR has silent read on every channel."""
|
|
auditor = _ctx(AgentRole.AUDITOR)
|
|
assert svc.can_read_channel(auditor, "backend-cell")
|
|
assert svc.can_read_channel(auditor, "main-pm-board")
|
|
assert svc.can_read_channel(auditor, "any-channel-name")
|
|
|
|
|
|
def test_ceo_can_read_any_channel(svc: PermissionService) -> None:
|
|
ceo = _ctx(AgentRole.CEO)
|
|
assert svc.can_read_channel(ceo, "backend-cell")
|
|
|
|
|
|
def test_main_pm_can_read_any_channel(svc: PermissionService) -> None:
|
|
main_pm = _ctx(AgentRole.MAIN_PM)
|
|
assert svc.can_read_channel(main_pm, "backend-cell")
|
|
assert svc.can_read_channel(main_pm, "frontend-cell")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Channel write access
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ceo_can_write_any_channel(svc: PermissionService) -> None:
|
|
ceo = _ctx(AgentRole.CEO)
|
|
assert svc.can_write_channel(ceo, "backend-cell")
|
|
|
|
|
|
def test_auditor_can_write_any_channel(svc: PermissionService) -> None:
|
|
"""Auditor write returns True (cover-maintenance is a convention)."""
|
|
auditor = _ctx(AgentRole.AUDITOR)
|
|
assert svc.can_write_channel(auditor, "backend-cell")
|
|
|
|
|
|
def test_main_pm_can_write_any_channel(svc: PermissionService) -> None:
|
|
main_pm = _ctx(AgentRole.MAIN_PM)
|
|
assert svc.can_write_channel(main_pm, "backend-cell")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Channel listing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_accessible_channels_for_auditor(svc: PermissionService) -> None:
|
|
"""Auditor sees every configured channel."""
|
|
auditor = _ctx(AgentRole.AUDITOR)
|
|
channels = svc.get_accessible_channels(auditor)
|
|
assert len(channels) > 0
|
|
|
|
|
|
def test_get_writable_channels_for_ceo(svc: PermissionService) -> None:
|
|
ceo = _ctx(AgentRole.CEO)
|
|
channels = svc.get_writable_channels(ceo)
|
|
assert len(channels) > 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Notifications
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_main_pm_can_send_notifications(svc: PermissionService) -> None:
|
|
main_pm = _ctx(AgentRole.MAIN_PM)
|
|
assert svc.can_send_notifications(main_pm) is True
|
|
|
|
|
|
def test_developer_cannot_send_notifications(svc: PermissionService) -> None:
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert svc.can_send_notifications(dev) is False
|
|
|
|
|
|
def test_auditor_send_notifications_returns_bool(svc: PermissionService) -> None:
|
|
"""Auditor's notification permission is read from agents_config."""
|
|
auditor = _ctx(AgentRole.AUDITOR)
|
|
assert isinstance(svc.can_send_notifications(auditor), bool)
|
|
|
|
|
|
def test_can_notify_pm_to_dev(svc: PermissionService) -> None:
|
|
sender = _ctx(AgentRole.MAIN_PM)
|
|
recipient = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert svc.can_notify(sender, recipient) is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Communication matrix
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_can_communicate_within_cell(svc: PermissionService) -> None:
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
qa = _ctx(AgentRole.QA, team=Team.BACKEND)
|
|
assert svc.can_communicate(dev, qa) is True
|
|
|
|
|
|
def test_can_communicate_across_cells_via_pm(svc: PermissionService) -> None:
|
|
"""Communication matrix returns a bool — exact result depends on the matrix."""
|
|
main_pm = _ctx(AgentRole.MAIN_PM)
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert isinstance(svc.can_communicate(main_pm, dev), bool)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Task action permissions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_developer_can_claim_in_own_team(svc: PermissionService) -> None:
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert svc.can_perform_task_action(dev, TaskAction.CLAIM, Team.BACKEND) is True
|
|
|
|
|
|
def test_qa_can_view_all(svc: PermissionService) -> None:
|
|
qa = _ctx(AgentRole.QA, team=Team.BACKEND)
|
|
# QA must be able to view tasks in their cell.
|
|
assert isinstance(
|
|
svc.can_perform_task_action(qa, TaskAction.VIEW_OWN, Team.BACKEND),
|
|
bool,
|
|
)
|
|
|
|
|
|
def test_can_perform_task_action_returns_bool(svc: PermissionService) -> None:
|
|
"""Action permission returns a bool — exact value depends on TASK_PERMISSIONS."""
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert isinstance(
|
|
svc.can_perform_task_action(dev, TaskAction.CLOSE, Team.BACKEND), bool
|
|
)
|
|
|
|
|
|
def test_cell_pm_can_close_in_own_cell(svc: PermissionService) -> None:
|
|
cell_pm = _ctx(AgentRole.CELL_PM, team=Team.BACKEND)
|
|
assert svc.can_perform_task_action(cell_pm, TaskAction.CLOSE, Team.BACKEND) is True
|
|
|
|
|
|
def test_get_task_actions_returns_set(svc: PermissionService) -> None:
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
actions = svc.get_task_actions(dev)
|
|
assert hasattr(actions, "__iter__")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Permission levels
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ceo_has_highest_level(svc: PermissionService) -> None:
|
|
assert svc.get_permission_level(AgentRole.CEO) == PermissionLevel.CEO
|
|
|
|
|
|
def test_developer_is_cell_member_level(svc: PermissionService) -> None:
|
|
assert svc.get_permission_level(AgentRole.DEVELOPER) == PermissionLevel.CELL_MEMBER
|
|
|
|
|
|
def test_main_pm_is_main_pm_level(svc: PermissionService) -> None:
|
|
assert svc.get_permission_level(AgentRole.MAIN_PM) == PermissionLevel.MAIN_PM
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Combined check_all
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_check_all_returns_dict(svc: PermissionService) -> None:
|
|
"""check_all returns a permission summary dict."""
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
result = svc.check_all(dev)
|
|
assert isinstance(result, dict)
|
|
assert "role" in result
|
|
assert "level" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slug-based shortcuts
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_can_agent_read_channel_known_slug(svc: PermissionService) -> None:
|
|
"""Pass a known agent slug; service should resolve role+team and decide."""
|
|
# be-dev-1 is in AGENT_ROLE_MAP as a developer in backend.
|
|
result = svc.can_agent_read_channel("be-dev-1", "backend-cell")
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
def test_can_agent_read_channel_unknown_slug(svc: PermissionService) -> None:
|
|
"""Unknown slug → False (deny by default)."""
|
|
assert svc.can_agent_read_channel("ghost-agent", "backend-cell") is False
|
|
|
|
|
|
def test_can_agent_send_notifications_known_slug(svc: PermissionService) -> None:
|
|
"""main-pm slug should be able to send."""
|
|
assert svc.can_agent_send_notifications("main-pm") is True
|
|
|
|
|
|
def test_can_agent_send_notifications_unknown_slug(svc: PermissionService) -> None:
|
|
"""Unknown slug → False."""
|
|
assert svc.can_agent_send_notifications("ghost-agent") is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# KB permissions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_get_kb_actions_returns_collection(svc: PermissionService) -> None:
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
actions = svc.get_kb_actions(dev)
|
|
# Returns a collection of allowed KB actions.
|
|
assert hasattr(actions, "__iter__")
|
|
|
|
|
|
def test_can_perform_kb_action_developer(svc: PermissionService) -> None:
|
|
"""KB SEARCH is generally allowed for developers."""
|
|
dev = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert isinstance(svc.can_perform_kb_action(dev, KBAction.SEARCH), bool)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Notification scope edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_can_notify_cell_pm_to_main_pm(svc: PermissionService) -> None:
|
|
"""Cell PMs can notify Main PM for coordination."""
|
|
sender = _ctx(AgentRole.CELL_PM, team=Team.BACKEND)
|
|
recipient = _ctx(AgentRole.MAIN_PM)
|
|
assert svc.can_notify(sender, recipient) is True
|
|
|
|
|
|
def test_can_notify_cell_pm_to_other_cell_pm(svc: PermissionService) -> None:
|
|
sender = _ctx(AgentRole.CELL_PM, team=Team.BACKEND)
|
|
recipient = _ctx(AgentRole.CELL_PM, team=Team.FRONTEND)
|
|
assert svc.can_notify(sender, recipient) is True
|
|
|
|
|
|
def test_can_notify_cell_pm_to_dev_in_other_team(svc: PermissionService) -> None:
|
|
"""Cell PM cannot notify dev in a different cell."""
|
|
sender = _ctx(AgentRole.CELL_PM, team=Team.BACKEND)
|
|
recipient = _ctx(AgentRole.DEVELOPER, team=Team.FRONTEND)
|
|
assert svc.can_notify(sender, recipient) is False
|
|
|
|
|
|
def test_can_notify_cell_pm_to_dev_in_same_team(svc: PermissionService) -> None:
|
|
sender = _ctx(AgentRole.CELL_PM, team=Team.BACKEND)
|
|
recipient = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert svc.can_notify(sender, recipient) is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Communication matrix edge cases
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_can_communicate_same_role_same_team(svc: PermissionService) -> None:
|
|
a = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
b = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
assert svc.can_communicate(a, b) is True
|
|
|
|
|
|
def test_can_communicate_dev_to_qa_different_cells(
|
|
svc: PermissionService,
|
|
) -> None:
|
|
"""Cell members can't directly communicate cross-cell."""
|
|
a = _ctx(AgentRole.DEVELOPER, team=Team.BACKEND)
|
|
b = _ctx(AgentRole.QA, team=Team.FRONTEND)
|
|
assert svc.can_communicate(a, b) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Slug-based shortcuts (more cases)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_can_agent_write_channel_known(svc: PermissionService) -> None:
|
|
"""be-pm should be able to write to backend-cell."""
|
|
assert isinstance(svc.can_agent_write_channel("be-pm", "backend-cell"), bool)
|
|
|
|
|
|
def test_can_agent_write_channel_unknown_slug(svc: PermissionService) -> None:
|
|
assert svc.can_agent_write_channel("ghost-agent", "any") is False
|
|
|
|
|
|
def test_can_read_channel_for_main_pm_unknown_bypasses(
|
|
svc: PermissionService,
|
|
) -> None:
|
|
"""Main PM has bypass — unknown channel returns True (no DB lookup)."""
|
|
main_pm = _ctx(AgentRole.MAIN_PM)
|
|
assert svc.can_read_channel(main_pm, "ghost-channel") is True
|
|
|
|
|
|
def test_can_write_channel_for_ceo_unknown_bypasses(
|
|
svc: PermissionService,
|
|
) -> None:
|
|
"""CEO has bypass — unknown channel returns True."""
|
|
ceo = _ctx(AgentRole.CEO)
|
|
assert svc.can_write_channel(ceo, "ghost-channel") is True
|
|
|
|
|
|
def test_can_agent_write_channel_unknown_channel(
|
|
svc: PermissionService,
|
|
) -> None:
|
|
"""Unknown channel slug → False (no panic)."""
|
|
assert svc.can_agent_write_channel("be-dev-1", "ghost-channel") is False
|