feat: add matrix routed platform facade
This commit is contained in:
parent
7627012f24
commit
242f4aadd3
3 changed files with 376 additions and 8 deletions
240
tests/adapter/matrix/test_routed_platform.py
Normal file
240
tests/adapter/matrix/test_routed_platform.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from adapter.matrix.bot import MatrixBot, build_runtime
|
||||
from adapter.matrix.routed_platform import RoutedPlatformClient
|
||||
from adapter.matrix.store import set_room_meta
|
||||
from core.chat import ChatManager
|
||||
from core.store import InMemoryStore
|
||||
from sdk.interface import MessageChunk, MessageResponse, User, UserSettings
|
||||
from sdk.mock import MockPlatformClient
|
||||
|
||||
|
||||
class FakeDelegate:
|
||||
def __init__(self, *, name: str) -> None:
|
||||
self.name = name
|
||||
self.send_calls: list[dict] = []
|
||||
self.stream_calls: list[dict] = []
|
||||
self.user_calls: list[dict] = []
|
||||
self.settings_calls: list[str] = []
|
||||
self.update_calls: list[tuple[str, object]] = []
|
||||
|
||||
async def get_or_create_user(
|
||||
self,
|
||||
external_id: str,
|
||||
platform: str,
|
||||
display_name: str | None = None,
|
||||
) -> User:
|
||||
self.user_calls.append(
|
||||
{
|
||||
"external_id": external_id,
|
||||
"platform": platform,
|
||||
"display_name": display_name,
|
||||
}
|
||||
)
|
||||
return User(
|
||||
user_id=f"user-{self.name}",
|
||||
external_id=external_id,
|
||||
platform=platform,
|
||||
display_name=display_name,
|
||||
created_at="2025-01-01T00:00:00Z",
|
||||
is_new=False,
|
||||
)
|
||||
|
||||
async def send_message(
|
||||
self,
|
||||
user_id: str,
|
||||
chat_id: str,
|
||||
text: str,
|
||||
attachments=None,
|
||||
) -> MessageResponse:
|
||||
self.send_calls.append(
|
||||
{
|
||||
"user_id": user_id,
|
||||
"chat_id": chat_id,
|
||||
"text": text,
|
||||
"attachments": attachments,
|
||||
}
|
||||
)
|
||||
return MessageResponse(
|
||||
message_id=f"msg-{self.name}",
|
||||
response=f"reply-{self.name}",
|
||||
tokens_used=0,
|
||||
finished=True,
|
||||
)
|
||||
|
||||
async def stream_message(
|
||||
self,
|
||||
user_id: str,
|
||||
chat_id: str,
|
||||
text: str,
|
||||
attachments=None,
|
||||
) -> AsyncIterator[MessageChunk]:
|
||||
self.stream_calls.append(
|
||||
{
|
||||
"user_id": user_id,
|
||||
"chat_id": chat_id,
|
||||
"text": text,
|
||||
"attachments": attachments,
|
||||
}
|
||||
)
|
||||
yield MessageChunk(
|
||||
message_id=f"stream-{self.name}",
|
||||
delta=f"delta-{self.name}",
|
||||
finished=True,
|
||||
tokens_used=0,
|
||||
)
|
||||
|
||||
async def get_settings(self, user_id: str) -> UserSettings:
|
||||
self.settings_calls.append(user_id)
|
||||
return UserSettings(skills={"files": True})
|
||||
|
||||
async def update_settings(self, user_id: str, action: object) -> None:
|
||||
self.update_calls.append((user_id, action))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_routes_by_room_agent_and_platform_chat_id():
|
||||
store = InMemoryStore()
|
||||
chat_mgr = ChatManager(None, store)
|
||||
await chat_mgr.get_or_create("u1", "C1", "matrix", "!room:example.org")
|
||||
await set_room_meta(
|
||||
store,
|
||||
"!room:example.org",
|
||||
{"platform_chat_id": "41", "agent_id": "agent-2"},
|
||||
)
|
||||
delegates = {
|
||||
"agent-1": FakeDelegate(name="agent-1"),
|
||||
"agent-2": FakeDelegate(name="agent-2"),
|
||||
}
|
||||
platform = RoutedPlatformClient(chat_mgr=chat_mgr, store=store, delegates=delegates)
|
||||
|
||||
response = await platform.send_message("u1", "C1", "hello", attachments=[])
|
||||
|
||||
assert response.response == "reply-agent-2"
|
||||
assert delegates["agent-1"].send_calls == []
|
||||
assert delegates["agent-2"].send_calls == [
|
||||
{
|
||||
"user_id": "u1",
|
||||
"chat_id": "41",
|
||||
"text": "hello",
|
||||
"attachments": [],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_message_routes_by_room_agent_and_platform_chat_id():
|
||||
store = InMemoryStore()
|
||||
chat_mgr = ChatManager(None, store)
|
||||
await chat_mgr.get_or_create("u1", "C1", "matrix", "!room:example.org")
|
||||
await set_room_meta(
|
||||
store,
|
||||
"!room:example.org",
|
||||
{"platform_chat_id": "41", "agent_id": "agent-2"},
|
||||
)
|
||||
delegates = {
|
||||
"agent-1": FakeDelegate(name="agent-1"),
|
||||
"agent-2": FakeDelegate(name="agent-2"),
|
||||
}
|
||||
platform = RoutedPlatformClient(chat_mgr=chat_mgr, store=store, delegates=delegates)
|
||||
|
||||
chunks = [chunk async for chunk in platform.stream_message("u1", "C1", "hello")]
|
||||
|
||||
assert [chunk.delta for chunk in chunks] == ["delta-agent-2"]
|
||||
assert delegates["agent-1"].stream_calls == []
|
||||
assert delegates["agent-2"].stream_calls == [
|
||||
{
|
||||
"user_id": "u1",
|
||||
"chat_id": "41",
|
||||
"text": "hello",
|
||||
"attachments": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_and_settings_delegate_to_default_client():
|
||||
store = InMemoryStore()
|
||||
chat_mgr = ChatManager(None, store)
|
||||
delegates = {
|
||||
"agent-1": FakeDelegate(name="agent-1"),
|
||||
"agent-2": FakeDelegate(name="agent-2"),
|
||||
}
|
||||
platform = RoutedPlatformClient(chat_mgr=chat_mgr, store=store, delegates=delegates)
|
||||
|
||||
user = await platform.get_or_create_user("ext-1", "matrix", display_name="Alice")
|
||||
settings = await platform.get_settings("u1")
|
||||
await platform.update_settings("u1", {"action": "noop"})
|
||||
|
||||
assert user.user_id == "user-agent-1"
|
||||
assert settings.skills == {"files": True}
|
||||
assert delegates["agent-1"].user_calls == [
|
||||
{
|
||||
"external_id": "ext-1",
|
||||
"platform": "matrix",
|
||||
"display_name": "Alice",
|
||||
}
|
||||
]
|
||||
assert delegates["agent-2"].user_calls == []
|
||||
assert delegates["agent-1"].settings_calls == ["u1"]
|
||||
assert delegates["agent-1"].update_calls == [("u1", {"action": "noop"})]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_runtime_real_backend_uses_routed_platform_with_registry(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
):
|
||||
registry_path = tmp_path / "matrix-agents.yaml"
|
||||
registry_path.write_text(
|
||||
"agents:\n"
|
||||
" - id: agent-1\n"
|
||||
" label: Analyst\n"
|
||||
" - id: agent-2\n"
|
||||
" label: Research\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
monkeypatch.setenv("MATRIX_PLATFORM_BACKEND", "real")
|
||||
monkeypatch.setenv("MATRIX_AGENT_REGISTRY_PATH", str(registry_path))
|
||||
monkeypatch.setenv("AGENT_BASE_URL", "http://agent.example")
|
||||
|
||||
runtime = build_runtime()
|
||||
|
||||
assert isinstance(runtime.platform, RoutedPlatformClient)
|
||||
assert set(runtime.platform._delegates) == {"agent-1", "agent-2"}
|
||||
assert runtime.platform._delegates["agent-1"].agent_base_url == "http://agent.example"
|
||||
assert runtime.platform._delegates["agent-1"].agent_id == "agent-1"
|
||||
assert runtime.platform._delegates["agent-2"].agent_id == "agent-2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_bot_keeps_local_chat_id_for_plain_message_dispatch():
|
||||
runtime = build_runtime(platform=MockPlatformClient())
|
||||
await set_room_meta(
|
||||
runtime.store,
|
||||
"!chat1:example.org",
|
||||
{
|
||||
"chat_id": "C1",
|
||||
"matrix_user_id": "@alice:example.org",
|
||||
"platform_chat_id": "41",
|
||||
"agent_id": "agent-2",
|
||||
},
|
||||
)
|
||||
runtime.dispatcher.dispatch = AsyncMock(return_value=[])
|
||||
bot = MatrixBot(SimpleNamespace(user_id="@bot:example.org"), runtime)
|
||||
|
||||
await bot.on_room_message(
|
||||
SimpleNamespace(room_id="!chat1:example.org"),
|
||||
SimpleNamespace(sender="@alice:example.org", body="hello"),
|
||||
)
|
||||
|
||||
dispatched = runtime.dispatcher.dispatch.await_args.args[0]
|
||||
assert dispatched.chat_id == "C1"
|
||||
assert dispatched.text == "hello"
|
||||
Loading…
Add table
Add a link
Reference in a new issue