Files
EvoScientist/tests/test_bus_integration.py
T
2026-02-15 21:07:47 +08:00

287 lines
9.1 KiB
Python

"""Tests for bus-mode agent integration (_bus_inbound_consumer)."""
import asyncio
from EvoScientist.channels.bus.events import InboundMessage
from EvoScientist.channels.bus.message_bus import MessageBus
from EvoScientist.channels.channel_manager import ChannelManager
from EvoScientist.channels.base import Channel, OutgoingMessage
def _run(coro):
"""Run an async coroutine safely, creating a fresh event loop."""
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
class _FakeConfig:
text_chunk_limit = 4096
allowed_senders = None
class FakeChannel(Channel):
"""Minimal channel for bus integration testing."""
name = "fake"
def __init__(self):
super().__init__(_FakeConfig())
self._started = False
self._stopped = False
self._sent: list[OutgoingMessage] = []
async def start(self):
self._started = True
async def stop(self):
self._stopped = True
async def receive(self):
while True:
try:
msg = await asyncio.wait_for(self._queue.get(), timeout=0.5)
yield msg
except asyncio.TimeoutError:
return
async def send(self, message: OutgoingMessage) -> bool:
self._sent.append(message)
return True
async def _send_chunk(self, chat_id, formatted_text, raw_text, reply_to, metadata):
pass
def _mock_stream_events(content, reply):
"""Create a mock stream_agent_events that yields text then done."""
async def _stream(agent, message, thread_id):
yield {"type": "text", "content": reply}
yield {"type": "done", "response": reply}
return _stream
def _mock_stream_events_error(error_msg):
"""Create a mock stream_agent_events that raises."""
async def _stream(agent, message, thread_id):
raise RuntimeError(error_msg)
yield # make it an async generator # pragma: no cover
return _stream
def _mock_stream_events_with_thinking(thinking_text, reply):
"""Create a mock stream_agent_events that yields thinking then done."""
async def _stream(agent, message, thread_id):
yield {"type": "thinking", "content": thinking_text}
yield {"type": "text", "content": reply}
yield {"type": "done", "content": reply}
return _stream
class TestBusInboundConsumer:
"""Test the _bus_inbound_consumer bridge function."""
def test_processes_inbound_and_publishes_outbound(self):
"""InboundMessage -> agent -> OutboundMessage flow."""
from EvoScientist.cli.channel import _bus_inbound_consumer
async def _test():
bus = MessageBus()
manager = ChannelManager(bus)
ch = FakeChannel()
manager.register(ch)
mock_stream = _mock_stream_events(
"hello agent", "Reply to: hello agent",
)
import EvoScientist.stream.events as events_mod
original = events_mod.stream_agent_events
events_mod.stream_agent_events = mock_stream
try:
consumer = asyncio.create_task(
_bus_inbound_consumer(bus, manager, None, "test-thread", False)
)
await bus.publish_inbound(InboundMessage(
channel="fake",
sender_id="user1",
chat_id="chat1",
content="hello agent",
))
await asyncio.sleep(0.5)
outbound = await asyncio.wait_for(
bus.consume_outbound(), timeout=2.0,
)
assert outbound.channel == "fake"
assert outbound.chat_id == "chat1"
assert "Reply to: hello agent" in outbound.content
consumer.cancel()
try:
await consumer
except asyncio.CancelledError:
pass
finally:
events_mod.stream_agent_events = original
_run(_test())
def test_agent_error_publishes_error_outbound(self):
"""When agent raises, an error message is published outbound."""
from EvoScientist.cli.channel import _bus_inbound_consumer
async def _test():
bus = MessageBus()
manager = ChannelManager(bus)
ch = FakeChannel()
manager.register(ch)
mock_stream = _mock_stream_events_error("agent crashed")
import EvoScientist.stream.events as events_mod
original = events_mod.stream_agent_events
events_mod.stream_agent_events = mock_stream
try:
consumer = asyncio.create_task(
_bus_inbound_consumer(bus, manager, None, "test-thread", False)
)
await bus.publish_inbound(InboundMessage(
channel="fake",
sender_id="user1",
chat_id="chat1",
content="crash me",
))
await asyncio.sleep(0.5)
outbound = await asyncio.wait_for(
bus.consume_outbound(), timeout=2.0,
)
assert outbound.channel == "fake"
assert "Error" in outbound.content or "error" in outbound.content.lower()
assert "agent crashed" in outbound.content
consumer.cancel()
try:
await consumer
except asyncio.CancelledError:
pass
finally:
events_mod.stream_agent_events = original
_run(_test())
def test_message_counting(self):
"""Messages are counted via record_message."""
from EvoScientist.cli.channel import _bus_inbound_consumer
async def _test():
bus = MessageBus()
manager = ChannelManager(bus)
ch = FakeChannel()
manager.register(ch)
mock_stream = _mock_stream_events("test", "ok")
import EvoScientist.stream.events as events_mod
original = events_mod.stream_agent_events
events_mod.stream_agent_events = mock_stream
try:
consumer = asyncio.create_task(
_bus_inbound_consumer(bus, manager, None, "test-thread", False)
)
await bus.publish_inbound(InboundMessage(
channel="fake",
sender_id="u1",
chat_id="c1",
content="test",
))
await asyncio.sleep(0.5)
await asyncio.wait_for(bus.consume_outbound(), timeout=2.0)
assert manager._message_counts["fake"]["received"] == 1
assert manager._message_counts["fake"]["sent"] == 1
consumer.cancel()
try:
await consumer
except asyncio.CancelledError:
pass
finally:
events_mod.stream_agent_events = original
_run(_test())
def test_thinking_sent_to_channel(self):
"""Thinking messages are sent to the channel when show_thinking=True."""
from EvoScientist.cli.channel import _bus_inbound_consumer
async def _test():
bus = MessageBus()
manager = ChannelManager(bus)
ch = FakeChannel()
server = manager.register(ch)
server.send_thinking = True
long_thinking = "A" * 250 # >= _MIN_THINKING_LEN (200)
mock_stream = _mock_stream_events_with_thinking(
long_thinking, "final answer",
)
import EvoScientist.stream.events as events_mod
original = events_mod.stream_agent_events
events_mod.stream_agent_events = mock_stream
try:
consumer = asyncio.create_task(
_bus_inbound_consumer(
bus, manager, None, "test-thread", True,
)
)
await bus.publish_inbound(InboundMessage(
channel="fake",
sender_id="user1",
chat_id="chat1",
content="think about this",
metadata={"chat_id": "chat1"},
))
await asyncio.sleep(0.5)
# Drain outbound (final answer)
outbound = await asyncio.wait_for(
bus.consume_outbound(), timeout=2.0,
)
assert "final answer" in outbound.content
# Check that thinking was sent via channel.send
thinking_msgs = [
m for m in ch._sent
if "\U0001f9e0" in m.content
]
assert len(thinking_msgs) == 1
assert long_thinking in thinking_msgs[0].content
consumer.cancel()
try:
await consumer
except asyncio.CancelledError:
pass
finally:
events_mod.stream_agent_events = original
_run(_test())