feat: implement channel runtime connectors

This commit is contained in:
2026-06-03 16:22:44 +08:00
parent ee972441f5
commit c3d84b904a
105 changed files with 15621 additions and 322 deletions

View File

@ -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