mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
100% Coverage
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""Coverage for roboco.events.bus thin wrapper functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from roboco.events.bus import EventBus, get_event_bus, init_event_bus
|
||||
from roboco.events.stream_bus import StreamEventBus
|
||||
|
||||
|
||||
def test_get_event_bus_delegates() -> None:
|
||||
"""get_event_bus() returns the underlying stream event bus singleton."""
|
||||
fake_bus = MagicMock()
|
||||
with patch(
|
||||
"roboco.events.bus.get_stream_event_bus", return_value=fake_bus
|
||||
) as mock_get:
|
||||
result = get_event_bus()
|
||||
mock_get.assert_called_once()
|
||||
assert result is fake_bus
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_event_bus_delegates() -> None:
|
||||
"""init_event_bus() forwards args to init_stream_event_bus (line 53)."""
|
||||
fake_bus = MagicMock()
|
||||
with patch(
|
||||
"roboco.events.bus.init_stream_event_bus",
|
||||
new_callable=AsyncMock,
|
||||
return_value=fake_bus,
|
||||
) as mock_init:
|
||||
result = await init_event_bus(consumer_name="custom", recover_pending=False)
|
||||
mock_init.assert_awaited_once_with(consumer_name="custom", recover_pending=False)
|
||||
assert result is fake_bus
|
||||
|
||||
|
||||
def test_event_bus_alias_is_stream_event_bus() -> None:
|
||||
"""EventBus is an alias for StreamEventBus."""
|
||||
|
||||
assert EventBus is StreamEventBus
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
@@ -11,11 +12,14 @@ from roboco.events.handlers import (
|
||||
_get_doc_id,
|
||||
_get_pm_id,
|
||||
_get_qa_id,
|
||||
get_event_context,
|
||||
handle_blocker_resolved,
|
||||
handle_handoff_created,
|
||||
handle_qa_result,
|
||||
handle_question_answered,
|
||||
handle_session_boundary,
|
||||
handle_task_status_change,
|
||||
register_default_handlers,
|
||||
set_event_context,
|
||||
)
|
||||
|
||||
@@ -30,9 +34,16 @@ def _make_event(event_type: EventType, **data) -> Event:
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_context():
|
||||
"""Reset event context after each test."""
|
||||
"""Reset event context after each test.
|
||||
|
||||
set_event_context only updates attrs when truthy, so we need to
|
||||
directly clear the underlying singleton EventContext to avoid
|
||||
leaking state between tests.
|
||||
"""
|
||||
yield
|
||||
set_event_context(notification_service=None, orchestrator=None)
|
||||
ctx = get_event_context()
|
||||
ctx.notification_service = None
|
||||
ctx.orchestrator = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -219,3 +230,219 @@ async def test_handle_blocker_resolved_logs() -> None:
|
||||
resolution="fixed",
|
||||
)
|
||||
await handle_blocker_resolved(event)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Awaiting docs handler — no-team early return.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_awaiting_docs_skips_when_no_team() -> None:
|
||||
notif = MagicMock()
|
||||
notif.send_docs_ready_notification = AsyncMock()
|
||||
set_event_context(notification_service=notif)
|
||||
event = _make_event(EventType.TASK_AWAITING_DOCS, task_id=str(uuid4()))
|
||||
await handle_task_status_change(event)
|
||||
notif.send_docs_ready_notification.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_awaiting_qa_skips_when_no_team() -> None:
|
||||
notif = MagicMock()
|
||||
notif.send_qa_ready_notification = AsyncMock()
|
||||
set_event_context(notification_service=notif)
|
||||
event = _make_event(EventType.TASK_AWAITING_QA, task_id=str(uuid4()))
|
||||
await handle_task_status_change(event)
|
||||
notif.send_qa_ready_notification.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_task_qa_failed_without_context_is_noop() -> None:
|
||||
"""No notification service set — TASK_QA_FAILED handler returns early."""
|
||||
event = _make_event(
|
||||
EventType.TASK_QA_FAILED, task_id=str(uuid4()), assigned_to="be-dev-1"
|
||||
)
|
||||
await handle_task_status_change(event)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_awaiting_qa_without_context_is_noop() -> None:
|
||||
event = _make_event(
|
||||
EventType.TASK_AWAITING_QA, task_id=str(uuid4()), team="backend"
|
||||
)
|
||||
await handle_task_status_change(event)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_awaiting_docs_without_context_is_noop() -> None:
|
||||
event = _make_event(
|
||||
EventType.TASK_AWAITING_DOCS, task_id=str(uuid4()), team="backend"
|
||||
)
|
||||
await handle_task_status_change(event)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# QA result + waiting agent resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@dataclass
|
||||
class _FakeWaitRecord:
|
||||
waiting_for: str
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qa_result_resolves_waiting_developer() -> None:
|
||||
"""When dev is waiting on `qa_result`, orchestrator.resolve_wait fires."""
|
||||
orch = MagicMock()
|
||||
orch.get_waiting_agents = MagicMock(
|
||||
return_value={"be-dev-1": _FakeWaitRecord(waiting_for="qa_result")}
|
||||
)
|
||||
orch.resolve_wait = AsyncMock()
|
||||
set_event_context(orchestrator=orch)
|
||||
|
||||
event = _make_event(
|
||||
EventType.TASK_QA_PASSED,
|
||||
task_id=str(uuid4()),
|
||||
assigned_to="be-dev-1",
|
||||
qa_notes="lgtm",
|
||||
)
|
||||
await handle_qa_result(event)
|
||||
orch.resolve_wait.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qa_result_does_not_resolve_when_not_waiting() -> None:
|
||||
orch = MagicMock()
|
||||
orch.get_waiting_agents = MagicMock(return_value={})
|
||||
orch.resolve_wait = AsyncMock()
|
||||
set_event_context(orchestrator=orch)
|
||||
|
||||
event = _make_event(
|
||||
EventType.TASK_QA_PASSED,
|
||||
task_id=str(uuid4()),
|
||||
assigned_to="be-dev-1",
|
||||
)
|
||||
await handle_qa_result(event)
|
||||
orch.resolve_wait.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qa_result_skips_when_developer_id_missing() -> None:
|
||||
"""Without `assigned_to`, _try_resolve_agent_wait short-circuits."""
|
||||
orch = MagicMock()
|
||||
orch.get_waiting_agents = MagicMock(return_value={})
|
||||
orch.resolve_wait = AsyncMock()
|
||||
set_event_context(orchestrator=orch)
|
||||
|
||||
event = _make_event(EventType.TASK_QA_PASSED, task_id=str(uuid4()))
|
||||
await handle_qa_result(event)
|
||||
orch.resolve_wait.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qa_result_no_orchestrator_is_noop() -> None:
|
||||
event = _make_event(
|
||||
EventType.TASK_QA_PASSED,
|
||||
task_id=str(uuid4()),
|
||||
assigned_to="be-dev-1",
|
||||
)
|
||||
# No orchestrator wired in.
|
||||
await handle_qa_result(event)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_qa_result_waiting_for_other_thing_is_noop() -> None:
|
||||
"""Dev is waiting, but for blocker_resolution — qa_result event ignores them."""
|
||||
orch = MagicMock()
|
||||
orch.get_waiting_agents = MagicMock(
|
||||
return_value={"be-dev-1": _FakeWaitRecord(waiting_for="blocker_resolution")}
|
||||
)
|
||||
orch.resolve_wait = AsyncMock()
|
||||
set_event_context(orchestrator=orch)
|
||||
|
||||
event = _make_event(
|
||||
EventType.TASK_QA_PASSED,
|
||||
task_id=str(uuid4()),
|
||||
assigned_to="be-dev-1",
|
||||
)
|
||||
await handle_qa_result(event)
|
||||
orch.resolve_wait.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Question answered handler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_question_answered_resolves_waiting_agent() -> None:
|
||||
orch = MagicMock()
|
||||
orch.get_waiting_agents = MagicMock(
|
||||
return_value={"be-dev-1": _FakeWaitRecord(waiting_for="answer")}
|
||||
)
|
||||
orch.resolve_wait = AsyncMock()
|
||||
set_event_context(orchestrator=orch)
|
||||
|
||||
event = _make_event(
|
||||
EventType.QUESTION_ANSWERED,
|
||||
question_id=str(uuid4()),
|
||||
asking_agent="be-dev-1",
|
||||
answer="42",
|
||||
)
|
||||
await handle_question_answered(event)
|
||||
orch.resolve_wait.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_blocker_resolved_resolves_waiting_agent() -> None:
|
||||
orch = MagicMock()
|
||||
orch.get_waiting_agents = MagicMock(
|
||||
return_value={"be-dev-1": _FakeWaitRecord(waiting_for="blocker_resolution")}
|
||||
)
|
||||
orch.resolve_wait = AsyncMock()
|
||||
set_event_context(orchestrator=orch)
|
||||
|
||||
event = _make_event(
|
||||
EventType.BLOCKER_RESOLVED,
|
||||
task_id=str(uuid4()),
|
||||
agent_id="be-dev-1",
|
||||
resolution="fixed",
|
||||
)
|
||||
await handle_blocker_resolved(event)
|
||||
orch.resolve_wait.assert_awaited_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_event_context + register_default_handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_event_context_returns_singleton() -> None:
|
||||
a = get_event_context()
|
||||
b = get_event_context()
|
||||
assert a is b
|
||||
|
||||
|
||||
def test_register_default_handlers_subscribes_each_event() -> None:
|
||||
"""register_default_handlers wires every documented EventType."""
|
||||
bus = MagicMock()
|
||||
bus.subscribe = MagicMock()
|
||||
register_default_handlers(bus=bus)
|
||||
|
||||
subscribed = [call.args[0] for call in bus.subscribe.call_args_list]
|
||||
assert EventType.TASK_BLOCKED in subscribed
|
||||
assert EventType.TASK_QA_PASSED in subscribed
|
||||
assert EventType.HANDOFF_CREATED in subscribed
|
||||
assert EventType.QUESTION_ANSWERED in subscribed
|
||||
|
||||
|
||||
def test_register_default_handlers_uses_global_bus_when_none_passed() -> None:
|
||||
"""When `bus=None`, it falls back to `get_event_bus()`."""
|
||||
fake_bus = MagicMock()
|
||||
fake_bus.subscribe = MagicMock()
|
||||
|
||||
with patch("roboco.events.handlers.get_event_bus", return_value=fake_bus):
|
||||
register_default_handlers()
|
||||
fake_bus.subscribe.assert_called()
|
||||
|
||||
Reference in New Issue
Block a user