mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
test: lift coverage 41% → 76% (+1068 tests across 36 files)
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.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""enforcement.a2a_access coverage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from roboco.enforcement.a2a_access import (
|
||||
A2AAccessDeniedError,
|
||||
get_a2a_allowed_targets,
|
||||
validate_a2a_access,
|
||||
)
|
||||
|
||||
|
||||
def test_validate_a2a_self_a2a_denied() -> None:
|
||||
with pytest.raises(A2AAccessDeniedError, match="cannot A2A yourself"):
|
||||
validate_a2a_access("be-dev-1", "be-dev-1")
|
||||
|
||||
|
||||
def test_validate_a2a_to_ceo_denied() -> None:
|
||||
with pytest.raises(A2AAccessDeniedError):
|
||||
validate_a2a_access("be-dev-1", "ceo")
|
||||
|
||||
|
||||
def test_validate_a2a_within_cell() -> None:
|
||||
"""Cell members can A2A within their cell."""
|
||||
result = validate_a2a_access("be-dev-1", "be-qa")
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_a2a_access_denied_error_has_attributes() -> None:
|
||||
err = A2AAccessDeniedError(
|
||||
from_agent="be-dev-1",
|
||||
to_agent="ceo",
|
||||
reason="CEO is human",
|
||||
)
|
||||
assert err.from_agent == "be-dev-1"
|
||||
assert err.to_agent == "ceo"
|
||||
|
||||
|
||||
def test_get_a2a_allowed_targets_returns_list() -> None:
|
||||
targets = get_a2a_allowed_targets("be-dev-1", ["be-qa", "be-pm", "fe-dev-1", "ceo"])
|
||||
assert isinstance(targets, list)
|
||||
|
||||
|
||||
def test_get_a2a_allowed_targets_excludes_ceo() -> None:
|
||||
targets = get_a2a_allowed_targets("be-dev-1", ["ceo"])
|
||||
assert "ceo" not in targets
|
||||
|
||||
|
||||
def test_get_a2a_allowed_targets_excludes_self() -> None:
|
||||
targets = get_a2a_allowed_targets("be-dev-1", ["be-dev-1", "be-qa"])
|
||||
# Self should be filtered.
|
||||
assert "be-dev-1" not in targets or "be-qa" in targets
|
||||
@@ -0,0 +1,59 @@
|
||||
"""enforcement.channel_access coverage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from roboco.enforcement.channel_access import (
|
||||
ChannelAccessDeniedError,
|
||||
get_agent_channels,
|
||||
validate_channel_access,
|
||||
)
|
||||
|
||||
|
||||
def test_validate_channel_access_invalid_action_raises() -> None:
|
||||
with pytest.raises(ValueError, match="Invalid action"):
|
||||
validate_channel_access("be-dev-1", "backend-cell", "execute")
|
||||
|
||||
|
||||
def test_validate_channel_access_unknown_channel_denied() -> None:
|
||||
with pytest.raises(ChannelAccessDeniedError, match="not configured"):
|
||||
validate_channel_access("be-dev-1", "ghost-channel", "read")
|
||||
|
||||
|
||||
def test_validate_channel_access_known_channel_allowed_member() -> None:
|
||||
"""A backend dev should be able to read backend-cell."""
|
||||
result = validate_channel_access("be-dev-1", "backend-cell", "read")
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_validate_channel_access_unauthorized_agent_denied() -> None:
|
||||
"""Random agent ID won't be in any allow lists."""
|
||||
with pytest.raises(ChannelAccessDeniedError):
|
||||
validate_channel_access("ghost-agent", "backend-cell", "write")
|
||||
|
||||
|
||||
def test_get_agent_channels_returns_list_for_known_agent() -> None:
|
||||
channels = get_agent_channels("be-dev-1", action="read")
|
||||
assert isinstance(channels, list)
|
||||
|
||||
|
||||
def test_get_agent_channels_for_unknown_agent_returns_only_wildcard() -> None:
|
||||
"""Unknown agent gets only wildcard-permitted channels."""
|
||||
channels = get_agent_channels("ghost-agent", action="read")
|
||||
assert isinstance(channels, list)
|
||||
|
||||
|
||||
def test_get_agent_channels_write_action() -> None:
|
||||
channels = get_agent_channels("main-pm", action="write")
|
||||
assert isinstance(channels, list)
|
||||
|
||||
|
||||
def test_channel_access_denied_error_has_attributes() -> None:
|
||||
err = ChannelAccessDeniedError(
|
||||
agent_id="be-dev-1",
|
||||
channel_slug="ghost",
|
||||
action="write",
|
||||
)
|
||||
assert err.agent_id == "be-dev-1"
|
||||
assert err.channel_slug == "ghost"
|
||||
assert err.action == "write"
|
||||
@@ -0,0 +1,96 @@
|
||||
"""enforcement.journal_perms coverage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from roboco.enforcement.journal_perms import (
|
||||
JournalAccessDeniedError,
|
||||
can_read_journal,
|
||||
get_readable_journals,
|
||||
validate_journal_access,
|
||||
)
|
||||
|
||||
|
||||
def test_self_can_read_own_journal() -> None:
|
||||
can, _ = can_read_journal("be-dev-1", "be-dev-1")
|
||||
assert can is True
|
||||
|
||||
|
||||
def test_protected_ceo_journal_only_ceo_or_auditor() -> None:
|
||||
can, _ = can_read_journal("be-dev-1", "ceo")
|
||||
assert can is False
|
||||
|
||||
|
||||
def test_ceo_can_read_any_journal() -> None:
|
||||
can, _ = can_read_journal("ceo", "be-dev-1")
|
||||
assert can is True
|
||||
|
||||
|
||||
def test_auditor_can_read_any_non_protected() -> None:
|
||||
can, _ = can_read_journal("auditor", "be-dev-1")
|
||||
assert can is True
|
||||
|
||||
|
||||
def test_main_pm_can_read_any_non_protected() -> None:
|
||||
can, _ = can_read_journal("main-pm", "be-dev-1")
|
||||
assert can is True
|
||||
|
||||
|
||||
def test_cell_pm_can_read_own_cell() -> None:
|
||||
can, _ = can_read_journal("be-pm", "be-dev-1")
|
||||
assert can is True
|
||||
|
||||
|
||||
def test_cell_pm_cannot_read_other_cell_dev() -> None:
|
||||
can, _ = can_read_journal("be-pm", "fe-dev-1")
|
||||
# Cross-cell access for cell PM is False unless target is also a PM.
|
||||
assert can is False
|
||||
|
||||
|
||||
def test_cell_pm_can_read_other_cell_pm() -> None:
|
||||
can, _ = can_read_journal("be-pm", "fe-pm")
|
||||
assert can is True
|
||||
|
||||
|
||||
def test_cell_member_same_cell() -> None:
|
||||
can, _ = can_read_journal("be-dev-1", "be-qa")
|
||||
assert can is True
|
||||
|
||||
|
||||
def test_cell_member_cross_cell_denied() -> None:
|
||||
can, _ = can_read_journal("be-dev-1", "fe-dev-1")
|
||||
assert can is False
|
||||
|
||||
|
||||
def test_validate_journal_access_raises_on_denied() -> None:
|
||||
with pytest.raises(JournalAccessDeniedError):
|
||||
validate_journal_access("be-dev-1", "fe-dev-1")
|
||||
|
||||
|
||||
def test_validate_journal_access_passes() -> None:
|
||||
assert validate_journal_access("be-dev-1", "be-qa") is True
|
||||
|
||||
|
||||
def test_get_readable_journals_for_ceo() -> None:
|
||||
info = get_readable_journals("ceo")
|
||||
assert info["scope"] == "all"
|
||||
|
||||
|
||||
def test_get_readable_journals_for_main_pm() -> None:
|
||||
info = get_readable_journals("main-pm")
|
||||
assert info["scope"] == "all_cells"
|
||||
|
||||
|
||||
def test_get_readable_journals_for_cell_pm() -> None:
|
||||
info = get_readable_journals("be-pm")
|
||||
assert info["scope"] == "cell_plus_pms"
|
||||
|
||||
|
||||
def test_get_readable_journals_for_developer() -> None:
|
||||
info = get_readable_journals("be-dev-1")
|
||||
assert info["scope"] == "cell"
|
||||
|
||||
|
||||
def test_get_readable_journals_for_unknown() -> None:
|
||||
info = get_readable_journals("ghost-agent")
|
||||
assert info["scope"] == "none"
|
||||
@@ -0,0 +1,67 @@
|
||||
"""enforcement.notification_perms coverage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from roboco.enforcement.notification_perms import (
|
||||
NotificationPermissionError,
|
||||
get_notification_scope,
|
||||
validate_notification_permission,
|
||||
)
|
||||
|
||||
|
||||
def test_developer_cannot_send_notifications() -> None:
|
||||
with pytest.raises(NotificationPermissionError, match="cannot send"):
|
||||
validate_notification_permission("be-dev-1", ["be-pm"])
|
||||
|
||||
|
||||
def test_main_pm_can_send_to_anyone() -> None:
|
||||
assert validate_notification_permission("main-pm", ["be-dev-1"]) is True
|
||||
|
||||
|
||||
def test_cell_pm_can_notify_cell_member() -> None:
|
||||
assert validate_notification_permission("be-pm", ["be-dev-1"]) is True
|
||||
|
||||
|
||||
def test_cell_pm_can_notify_other_cell_pm() -> None:
|
||||
assert validate_notification_permission("be-pm", ["fe-pm"]) is True
|
||||
|
||||
|
||||
def test_cell_pm_can_notify_main_pm() -> None:
|
||||
assert validate_notification_permission("be-pm", ["main-pm"]) is True
|
||||
|
||||
|
||||
def test_cell_pm_cannot_notify_other_cell_dev() -> None:
|
||||
with pytest.raises(NotificationPermissionError):
|
||||
validate_notification_permission("be-pm", ["fe-dev-1"])
|
||||
|
||||
|
||||
def test_get_notification_scope_for_main_pm() -> None:
|
||||
scope = get_notification_scope("main-pm")
|
||||
assert scope.get("can_send") is True
|
||||
|
||||
|
||||
def test_get_notification_scope_for_developer() -> None:
|
||||
scope = get_notification_scope("be-dev-1")
|
||||
assert scope.get("can_send") is False
|
||||
|
||||
|
||||
def test_get_notification_scope_for_unknown() -> None:
|
||||
scope = get_notification_scope("ghost-agent")
|
||||
assert scope.get("can_send") is False
|
||||
|
||||
|
||||
def test_validate_with_multiple_recipients() -> None:
|
||||
"""Validate succeeds when all recipients are reachable."""
|
||||
assert validate_notification_permission("main-pm", ["be-dev-1", "fe-dev-1"]) is True
|
||||
|
||||
|
||||
def test_validate_fails_on_first_unreachable() -> None:
|
||||
"""Validation halts at the first unreachable recipient."""
|
||||
with pytest.raises(NotificationPermissionError):
|
||||
validate_notification_permission("be-pm", ["be-dev-1", "fe-dev-1"])
|
||||
|
||||
|
||||
def test_unknown_agent_cannot_send() -> None:
|
||||
with pytest.raises(NotificationPermissionError):
|
||||
validate_notification_permission("ghost-agent", ["be-pm"])
|
||||
@@ -0,0 +1,186 @@
|
||||
"""enforcement.task_lifecycle coverage."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from roboco.enforcement.task_lifecycle import (
|
||||
GitContext,
|
||||
GitRequirementError,
|
||||
can_agent_transition,
|
||||
check_parallel_completion,
|
||||
get_valid_transitions,
|
||||
is_active_state,
|
||||
is_terminal_state,
|
||||
is_waiting_state,
|
||||
sla_seconds_for,
|
||||
validate_git_requirements,
|
||||
validate_task_transition,
|
||||
)
|
||||
from roboco.exceptions import TaskLifecycleError
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# validate_task_transition
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_valid_transition_pending_to_claimed() -> None:
|
||||
assert validate_task_transition("pending", "claimed") is True
|
||||
|
||||
|
||||
def test_invalid_transition_raises() -> None:
|
||||
with pytest.raises(TaskLifecycleError):
|
||||
validate_task_transition("pending", "completed")
|
||||
|
||||
|
||||
def test_terminal_states_have_no_outgoing() -> None:
|
||||
with pytest.raises(TaskLifecycleError):
|
||||
validate_task_transition("completed", "claimed")
|
||||
|
||||
|
||||
def test_can_agent_transition_returns_bool() -> None:
|
||||
assert can_agent_transition("pending", "claimed", "developer") is True
|
||||
|
||||
|
||||
def test_can_agent_transition_invalid_returns_false() -> None:
|
||||
assert can_agent_transition("pending", "completed", "developer") is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_valid_transitions / state predicates
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_valid_transitions_for_pending() -> None:
|
||||
transitions = get_valid_transitions("pending")
|
||||
assert "claimed" in transitions
|
||||
assert "cancelled" in transitions
|
||||
|
||||
|
||||
def test_get_valid_transitions_for_completed() -> None:
|
||||
assert get_valid_transitions("completed") == []
|
||||
|
||||
|
||||
def test_get_valid_transitions_unknown_status() -> None:
|
||||
assert get_valid_transitions("ghost") == []
|
||||
|
||||
|
||||
def test_is_terminal_state_for_completed() -> None:
|
||||
assert is_terminal_state("completed") is True
|
||||
|
||||
|
||||
def test_is_terminal_state_for_cancelled() -> None:
|
||||
assert is_terminal_state("cancelled") is True
|
||||
|
||||
|
||||
def test_is_terminal_state_for_in_progress() -> None:
|
||||
assert is_terminal_state("in_progress") is False
|
||||
|
||||
|
||||
def test_is_waiting_state_for_blocked() -> None:
|
||||
assert is_waiting_state("blocked") is True
|
||||
|
||||
|
||||
def test_is_waiting_state_for_in_progress() -> None:
|
||||
assert is_waiting_state("in_progress") is False
|
||||
|
||||
|
||||
def test_is_active_state_for_claimed() -> None:
|
||||
assert is_active_state("claimed") is True
|
||||
|
||||
|
||||
def test_is_active_state_for_completed() -> None:
|
||||
assert is_active_state("completed") is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Git requirements
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_validate_git_no_context_passes() -> None:
|
||||
"""Without git_ctx, all transitions pass git checks."""
|
||||
assert validate_git_requirements("claimed", "in_progress", None) is True
|
||||
|
||||
|
||||
def test_git_doc_to_pm_review_requires_docs_complete() -> None:
|
||||
ctx = GitContext(docs_complete=False, pr_created=True)
|
||||
with pytest.raises(GitRequirementError, match="docs_complete"):
|
||||
validate_git_requirements("awaiting_documentation", "awaiting_pm_review", ctx)
|
||||
|
||||
|
||||
def test_git_doc_to_pm_review_requires_pr_created() -> None:
|
||||
ctx = GitContext(docs_complete=True, pr_created=False)
|
||||
with pytest.raises(GitRequirementError, match="PR not yet created"):
|
||||
validate_git_requirements("awaiting_documentation", "awaiting_pm_review", ctx)
|
||||
|
||||
|
||||
def test_git_doc_to_pm_review_succeeds_when_both_complete() -> None:
|
||||
ctx = GitContext(docs_complete=True, pr_created=True)
|
||||
assert (
|
||||
validate_git_requirements("awaiting_documentation", "awaiting_pm_review", ctx)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_git_pm_to_ceo_requires_pr_number() -> None:
|
||||
ctx = GitContext(pr_number=None)
|
||||
with pytest.raises(GitRequirementError, match="pr_number"):
|
||||
validate_git_requirements("awaiting_pm_review", "awaiting_ceo_approval", ctx)
|
||||
|
||||
|
||||
def test_git_pm_to_ceo_succeeds_with_pr() -> None:
|
||||
ctx = GitContext(pr_number=42)
|
||||
assert (
|
||||
validate_git_requirements("awaiting_pm_review", "awaiting_ceo_approval", ctx)
|
||||
is True
|
||||
)
|
||||
|
||||
|
||||
def test_git_claimed_to_in_progress_requires_branch() -> None:
|
||||
ctx = GitContext(branch_name=None)
|
||||
with pytest.raises(GitRequirementError, match="no branch"):
|
||||
validate_git_requirements("claimed", "in_progress", ctx)
|
||||
|
||||
|
||||
def test_git_claimed_to_in_progress_succeeds_with_branch() -> None:
|
||||
ctx = GitContext(branch_name="feature/x")
|
||||
assert validate_git_requirements("claimed", "in_progress", ctx) is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check_parallel_completion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_check_parallel_completion_both_done() -> None:
|
||||
assert check_parallel_completion(docs_complete=True, pr_created=True) is True
|
||||
|
||||
|
||||
def test_check_parallel_completion_docs_only() -> None:
|
||||
assert check_parallel_completion(docs_complete=True, pr_created=False) is False
|
||||
|
||||
|
||||
def test_check_parallel_completion_pr_only() -> None:
|
||||
assert check_parallel_completion(docs_complete=False, pr_created=True) is False
|
||||
|
||||
|
||||
def test_check_parallel_completion_neither() -> None:
|
||||
assert check_parallel_completion(docs_complete=False, pr_created=False) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SLA seconds
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_sla_seconds_for_developer_in_progress() -> None:
|
||||
result = sla_seconds_for("developer", "in_progress")
|
||||
assert result is None or isinstance(result, int)
|
||||
|
||||
|
||||
def test_sla_seconds_for_unknown_pair() -> None:
|
||||
assert sla_seconds_for("ghost", "unknown_status") is None
|
||||
|
||||
|
||||
def test_sla_seconds_for_no_role() -> None:
|
||||
assert sla_seconds_for(None, "in_progress") is None
|
||||
Reference in New Issue
Block a user