feat: implement channel runtime connectors
This commit is contained in:
@ -2,9 +2,10 @@ import asyncio
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from beaver.foundation.events import InboundMessage, MessageBus
|
||||
from beaver.foundation.events import InboundMessage, MessageBus, OutboundMessage
|
||||
from beaver.interfaces.channels import ChannelManager, MemoryChannelAdapter
|
||||
from beaver.interfaces.gateway.main import run_gateway
|
||||
from beaver.interfaces.channels.runtime import ChannelRuntime
|
||||
from beaver.services.agent_service import AgentService
|
||||
|
||||
|
||||
@ -52,22 +53,15 @@ class InvalidService:
|
||||
is_running = True
|
||||
|
||||
|
||||
def test_gateway_routes_memory_channel_roundtrip() -> None:
|
||||
def test_gateway_routes_memory_channel_roundtrip(tmp_path) -> None:
|
||||
async def run() -> None:
|
||||
bus = MessageBus()
|
||||
channel = MemoryChannelAdapter(bus)
|
||||
stop_event = asyncio.Event()
|
||||
task = asyncio.create_task(
|
||||
run_gateway(
|
||||
service=FakeService(),
|
||||
manage_service_lifecycle=False,
|
||||
bus=bus,
|
||||
channels=[channel],
|
||||
stop_event=stop_event,
|
||||
)
|
||||
)
|
||||
runtime = ChannelRuntime(service=FakeService(), bus=bus, channels={}, workspace=tmp_path)
|
||||
channel = MemoryChannelAdapter(runtime)
|
||||
runtime.manager.register(channel)
|
||||
await runtime.start()
|
||||
|
||||
await channel.publish_text("hello", session_id="s1")
|
||||
await channel.publish_text("hello", peer_id="s1", message_id="m1")
|
||||
for _ in range(40):
|
||||
if channel.sent_messages:
|
||||
break
|
||||
@ -76,38 +70,73 @@ def test_gateway_routes_memory_channel_roundtrip() -> None:
|
||||
assert channel.sent_messages
|
||||
message = channel.sent_messages[0]
|
||||
assert message.content == "echo:hello"
|
||||
assert message.session_id == "s1"
|
||||
assert message.session_id == "memory-dev:memory:s1"
|
||||
assert message.finish_reason == "stop"
|
||||
assert message.metadata["task_id"] == "task-1"
|
||||
assert message.metadata["task_status"] == "awaiting_acceptance"
|
||||
assert message.metadata["evidence_status"] == "recorded"
|
||||
assert message.metadata["validation_result"] is None
|
||||
|
||||
stop_event.set()
|
||||
await asyncio.wait_for(task, timeout=2)
|
||||
await runtime.stop()
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_gateway_delivers_cancelled_outbound_to_channel() -> None:
|
||||
def test_channel_manager_dispatches_by_channel_id() -> None:
|
||||
class CaptureChannel:
|
||||
channel_id = "webhook-dev"
|
||||
kind = "webhook"
|
||||
mode = "webhook"
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.sent = []
|
||||
|
||||
async def start(self) -> None:
|
||||
pass
|
||||
|
||||
async def stop(self) -> None:
|
||||
pass
|
||||
|
||||
async def send(self, message: Any) -> None:
|
||||
self.sent.append(message)
|
||||
|
||||
async def run() -> None:
|
||||
bus = MessageBus()
|
||||
channel = MemoryChannelAdapter(bus)
|
||||
stop_event = asyncio.Event()
|
||||
task = asyncio.create_task(
|
||||
run_gateway(
|
||||
service=SlowService(),
|
||||
manage_service_lifecycle=False,
|
||||
bus=bus,
|
||||
channels=[channel],
|
||||
stop_event=stop_event,
|
||||
channel = CaptureChannel()
|
||||
manager = ChannelManager(bus)
|
||||
manager.register(channel)
|
||||
await bus.publish_outbound(
|
||||
OutboundMessage(
|
||||
channel="webhook-dev",
|
||||
content="ok",
|
||||
session_id="webhook-dev:local:demo",
|
||||
finish_reason="stop",
|
||||
)
|
||||
)
|
||||
|
||||
await channel.publish_text("slow", session_id="s1")
|
||||
await asyncio.sleep(0.05)
|
||||
stop_event = asyncio.Event()
|
||||
stop_event.set()
|
||||
await asyncio.wait_for(task, timeout=3)
|
||||
|
||||
await manager.dispatch_outbound(stop_event)
|
||||
|
||||
assert channel.sent[0].content == "ok"
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_gateway_delivers_cancelled_outbound_to_channel(tmp_path) -> None:
|
||||
async def run() -> None:
|
||||
bus = MessageBus()
|
||||
runtime = ChannelRuntime(service=SlowService(), bus=bus, channels={}, workspace=tmp_path)
|
||||
channel = MemoryChannelAdapter(runtime)
|
||||
runtime.manager.register(channel)
|
||||
await runtime.start()
|
||||
|
||||
await channel.publish_text("slow", peer_id="s1", message_id="m1")
|
||||
for _ in range(40):
|
||||
if any(event["kind"] == "direct_run_started" for event in runtime.events.recent(limit=20)):
|
||||
break
|
||||
await asyncio.sleep(0.05)
|
||||
await runtime.stop()
|
||||
|
||||
assert channel.sent_messages
|
||||
assert channel.sent_messages[0].finish_reason == "cancelled"
|
||||
@ -118,13 +147,27 @@ def test_gateway_delivers_cancelled_outbound_to_channel() -> None:
|
||||
def test_gateway_rejects_channel_manager_and_channels_together() -> None:
|
||||
async def run() -> None:
|
||||
bus = MessageBus()
|
||||
class CaptureChannel:
|
||||
channel_id = "memory-dev"
|
||||
kind = "memory"
|
||||
mode = "webhook"
|
||||
|
||||
async def start(self) -> None:
|
||||
pass
|
||||
|
||||
async def stop(self) -> None:
|
||||
pass
|
||||
|
||||
async def send(self, message: Any) -> None:
|
||||
pass
|
||||
|
||||
try:
|
||||
await run_gateway(
|
||||
service=FakeService(),
|
||||
manage_service_lifecycle=False,
|
||||
bus=bus,
|
||||
channel_manager=ChannelManager(bus),
|
||||
channels=[MemoryChannelAdapter(bus)],
|
||||
channels=[CaptureChannel()],
|
||||
stop_event=asyncio.Event(),
|
||||
)
|
||||
except ValueError as exc:
|
||||
@ -212,10 +255,16 @@ def test_channel_manager_keeps_unknown_channel_outbound_undeliverable() -> None:
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_memory_channel_adapts_old_style_payload_to_stable_session_id() -> None:
|
||||
def test_memory_channel_adapts_payload_to_channel_identity_session_id(tmp_path) -> None:
|
||||
async def run() -> None:
|
||||
bus = MessageBus()
|
||||
channel = MemoryChannelAdapter(bus, name="telegram")
|
||||
runtime = ChannelRuntime(service=FakeService(), bus=bus, channels={}, workspace=tmp_path)
|
||||
channel = MemoryChannelAdapter(
|
||||
runtime,
|
||||
channel_id="telegram-main",
|
||||
kind="telegram",
|
||||
account_id="bot-main",
|
||||
)
|
||||
inbound = await channel.publish_external_text(
|
||||
"hello",
|
||||
chat_id="chat-1",
|
||||
@ -225,8 +274,10 @@ def test_memory_channel_adapts_old_style_payload_to_stable_session_id() -> None:
|
||||
|
||||
queued = await bus.consume_inbound()
|
||||
assert queued is inbound
|
||||
assert queued.channel == "telegram"
|
||||
assert queued.session_id == "telegram:chat-1"
|
||||
assert queued.channel == "telegram-main"
|
||||
assert queued.session_id == "telegram-main:bot-main:chat-1"
|
||||
assert queued.channel_identity is not None
|
||||
assert queued.channel_identity.kind == "telegram"
|
||||
assert queued.metadata["chat_id"] == "chat-1"
|
||||
assert queued.metadata["message_id"] == "message-1"
|
||||
assert queued.metadata["raw_channel_payload"] == {"platform": "telegram", "text": "hello"}
|
||||
@ -236,7 +287,9 @@ def test_memory_channel_adapts_old_style_payload_to_stable_session_id() -> None:
|
||||
|
||||
def test_channel_manager_start_cancellation_rolls_back_started_channels() -> None:
|
||||
class StartedChannel:
|
||||
name = "started"
|
||||
channel_id = "started"
|
||||
kind = "memory"
|
||||
mode = "webhook"
|
||||
|
||||
def __init__(self, bus: MessageBus) -> None:
|
||||
self.bus = bus
|
||||
@ -252,7 +305,9 @@ def test_channel_manager_start_cancellation_rolls_back_started_channels() -> Non
|
||||
pass
|
||||
|
||||
class BlockingChannel:
|
||||
name = "blocking"
|
||||
channel_id = "blocking"
|
||||
kind = "memory"
|
||||
mode = "webhook"
|
||||
|
||||
def __init__(self, bus: MessageBus) -> None:
|
||||
self.bus = bus
|
||||
|
||||
Reference in New Issue
Block a user