feat: route telegram eba events to plugins

This commit is contained in:
Junyan Qin
2026-05-07 17:02:49 +08:00
parent e4a471af18
commit 5c182c0f29
9 changed files with 573 additions and 14 deletions

View File

@@ -469,7 +469,9 @@ class PlatformSpecificEvent(Event):
| `friend.added` | N | Y | Y | N | N | N | Y | Y | N |
| `bot.invited_to_group` | Y | Y | Y | Y | Y | Y | Y | N | Y |
| `bot.removed_from_group` | Y | Y | Y | Y | N | N | Y | N | Y |
| `bot.muted` | N | N | Y | N | N | N | N | N | N |
| `bot.muted` | Y | N | Y | N | N | N | N | N | N |
| `bot.unmuted` | Y | N | Y | N | N | N | N | N | N |
| `platform.specific` | Y | Y | Y | Y | Y | Y | Y | Y | Y |
> 注:此表为初步评估,具体以各平台 SDK/API 文档为准,实施时逐个确认。
@@ -545,6 +547,9 @@ spec:
- group.info_updated
- bot.invited_to_group
- bot.removed_from_group
- bot.muted
- bot.unmuted
- platform.specific
platform_specific_events:
- chat_member_updated
- chat_join_request

View File

@@ -323,6 +323,9 @@ spec:
- group.info_updated
- bot.invited_to_group
- bot.removed_from_group
- bot.muted
- bot.unmuted
- platform.specific
supported_apis:
required:

View File

@@ -721,6 +721,9 @@ interface BotConfig {
- friend.added
- bot.invited_to_group
- bot.removed_from_group
- bot.muted
- bot.unmuted
- platform.specific
─────────────────
- message.* (所有消息事件)
- feedback.* (所有反馈事件)

View File

@@ -154,6 +154,9 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
'group.member_banned',
'bot.invited_to_group',
'bot.removed_from_group',
'bot.muted',
'bot.unmuted',
'platform.specific',
]
def get_supported_apis(self) -> list[str]:
@@ -221,27 +224,41 @@ class TelegramAdapter(TelegramAPIMixin, abstract_platform_adapter.AbstractPlatfo
components = await TelegramMessageConverter.yiri2target(message, self.bot)
for component in components:
if component['type'] == 'text':
component_type = component.get('type')
args = {
'chat_id': message_source.source_platform_object.effective_chat.id,
}
if message_source.source_platform_object.message.message_thread_id:
args['message_thread_id'] = message_source.source_platform_object.message.message_thread_id
if quote_origin:
args['reply_to_message_id'] = message_source.source_platform_object.message.id
if component_type == 'text':
if self.config['markdown_card'] is True:
content = telegramify_markdown.markdownify(
content=component['text'],
)
else:
content = component['text']
args = {
'chat_id': message_source.source_platform_object.effective_chat.id,
'text': content,
}
if self.config['markdown_card'] is True:
args['parse_mode'] = 'MarkdownV2'
if message_source.source_platform_object.message.message_thread_id:
args['message_thread_id'] = message_source.source_platform_object.message.message_thread_id
if quote_origin:
args['reply_to_message_id'] = message_source.source_platform_object.message.id
await self.bot.send_message(**args)
args['text'] = content
await self.bot.send_message(**args)
elif component_type == 'photo':
photo = component.get('photo')
if photo is None:
continue
args['photo'] = telegram.InputFile(photo)
await self.bot.send_photo(**args)
elif component_type == 'document':
doc = component.get('document')
if doc is None:
continue
filename = component.get('filename', 'file')
args['document'] = telegram.InputFile(doc, filename=filename)
await self.bot.send_document(**args)
# ---- Streaming Output (preserving original logic) ----

View File

@@ -310,6 +310,16 @@ class TelegramEventConverter(abstract_platform_adapter.AbstractEventConverter):
source_platform_object=update,
)
if old_status == 'restricted' and new_status in ('member', 'administrator') and mcm.new_chat_member:
return platform_events.BotUnmutedEvent(
type='bot.unmuted',
timestamp=mcm.date.timestamp() if mcm.date else time.time(),
adapter_name='telegram',
group=group,
operator=inviter,
source_platform_object=update,
)
return platform_events.PlatformSpecificEvent(
type='platform.specific',
timestamp=mcm.date.timestamp() if mcm.date else time.time(),

View File

@@ -47,6 +47,9 @@ spec:
- group.member_banned
- bot.invited_to_group
- bot.removed_from_group
- bot.muted
- bot.unmuted
- platform.specific
supported_apis:
required:

View File

@@ -18,6 +18,7 @@ from ..entity.errors import platform as platform_errors
from .logger import EventLogger
import langbot_plugin.api.entities.builtin.provider.session as provider_session
import langbot_plugin.api.entities.events as plugin_events
import langbot_plugin.api.entities.builtin.platform.events as platform_events
import langbot_plugin.api.entities.builtin.platform.message as platform_message
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
@@ -77,6 +78,31 @@ class RuntimeBot:
PIPELINE_DISCARD = '__discard__'
PIPELINE_DISCARD_DISPLAY_NAME = 'Discarded'
@staticmethod
def _eba_event_to_plugin_event(event: platform_events.EBAEvent) -> plugin_events.BaseEventModel | None:
"""Map a platform EBA event to a plugin EventListener event model."""
event_mapping: list[tuple[type[platform_events.EBAEvent], type[plugin_events.BaseEventModel]]] = [
(platform_events.MessageReceivedEvent, plugin_events.MessageReceived),
(platform_events.MessageEditedEvent, plugin_events.MessageEdited),
(platform_events.MessageDeletedEvent, plugin_events.MessageDeleted),
(platform_events.MessageReactionEvent, plugin_events.MessageReactionReceived),
(platform_events.FeedbackReceivedEvent, plugin_events.FeedbackReceived),
(platform_events.MemberJoinedEvent, plugin_events.GroupMemberJoined),
(platform_events.MemberLeftEvent, plugin_events.GroupMemberLeft),
(platform_events.MemberBannedEvent, plugin_events.GroupMemberBanned),
(platform_events.BotInvitedToGroupEvent, plugin_events.BotInvitedToGroup),
(platform_events.BotRemovedFromGroupEvent, plugin_events.BotRemovedFromGroup),
(platform_events.BotMutedEvent, plugin_events.BotMuted),
(platform_events.BotUnmutedEvent, plugin_events.BotUnmuted),
(platform_events.PlatformSpecificEvent, plugin_events.PlatformSpecificEventReceived),
]
for platform_event_type, plugin_event_type in event_mapping:
if isinstance(event, platform_event_type):
return plugin_event_type.from_platform_event(event)
return None
def resolve_pipeline_uuid(
self,
launcher_type: str,
@@ -366,6 +392,21 @@ class RuntimeBot:
self.adapter.register_listener(platform_events.FeedbackEvent, on_feedback)
async def on_eba_event(
event: platform_events.EBAEvent,
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
):
plugin_event = self._eba_event_to_plugin_event(event)
if plugin_event is None:
return
try:
await self.ap.plugin_connector.emit_event(plugin_event)
except Exception:
await self.logger.error(f'Failed to dispatch EBA event to plugins: {traceback.format_exc()}')
self.adapter.register_listener(platform_events.EBAEvent, on_eba_event)
async def run(self):
async def exception_wrapper():
try:

View File

@@ -0,0 +1,164 @@
from __future__ import annotations
import argparse
import asyncio
import base64
import json
import os
from pathlib import Path
import telegram
from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter
from langbot_plugin.api.definition.abstract.platform.event_logger import AbstractEventLogger
from langbot_plugin.api.entities.builtin.platform import events as platform_events
from langbot_plugin.api.entities.builtin.platform import message as platform_message
class ProbeLogger(AbstractEventLogger):
async def info(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[info] {text}')
async def debug(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[debug] {text}')
async def warning(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[warning] {text}')
async def error(self, text, images=None, message_session_id=None, no_throw=True):
print(f'[error] {text}')
PNG_1X1 = base64.b64decode(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
)
def summarize_event(event: platform_events.EBAEvent) -> dict:
data = {
'type': event.type,
'adapter_name': event.adapter_name,
'timestamp': event.timestamp,
}
for field in (
'message_id',
'chat_id',
'chat_type',
'reaction',
'is_add',
'action',
'data',
):
if hasattr(event, field):
value = getattr(event, field)
if hasattr(value, 'value'):
value = value.value
data[field] = value
return data
async def run_probe(token: str, log_path: Path, timeout: int):
adapter = TelegramAdapter(
{
'token': token,
'markdown_card': False,
'enable-stream-reply': False,
},
ProbeLogger(),
)
events: list[platform_events.EBAEvent] = []
first_message = asyncio.Event()
async def listener(event, adapter):
events.append(event)
log_path.parent.mkdir(parents=True, exist_ok=True)
with log_path.open('a', encoding='utf-8') as fp:
fp.write(json.dumps(summarize_event(event), ensure_ascii=False) + '\n')
print('TELEGRAM_EBA_EVENT', json.dumps(summarize_event(event), ensure_ascii=False))
if isinstance(event, platform_events.MessageReceivedEvent):
first_message.set()
adapter.register_listener(platform_events.EBAEvent, listener)
await adapter.run_async()
try:
print('READY: send a private or group message to the Telegram test bot now.')
await asyncio.wait_for(first_message.wait(), timeout=timeout)
source = next(event for event in events if isinstance(event, platform_events.MessageReceivedEvent))
await adapter.reply_message(
source,
platform_message.MessageChain(
[
platform_message.Plain(text='EBA live reply: text'),
platform_message.Image(base64=base64.b64encode(PNG_1X1).decode()),
platform_message.File(
name='eba-live.txt',
size=8,
base64='data:text/plain;base64,' + base64.b64encode(b'eba-live').decode(),
),
]
),
quote_origin=True,
)
await adapter.send_message(
source.chat_type.value if hasattr(source.chat_type, 'value') else str(source.chat_type),
source.chat_id,
platform_message.MessageChain([platform_message.Plain(text='EBA live send_message OK')]),
)
edit_probe = await adapter.bot.send_message(chat_id=source.chat_id, text='EBA edit/delete probe')
await adapter.edit_message(
str(source.chat_type),
source.chat_id,
edit_probe.message_id,
platform_message.MessageChain([platform_message.Plain(text='EBA edit probe edited')]),
)
await adapter.delete_message(str(source.chat_type), source.chat_id, edit_probe.message_id)
await adapter.bot.send_message(
chat_id=source.chat_id,
text='EBA callback probe',
reply_markup=telegram.InlineKeyboardMarkup(
[[telegram.InlineKeyboardButton('callback probe', callback_data='eba-callback-probe')]]
),
)
if str(source.chat_type) == 'ChatType.GROUP' or getattr(source.chat_type, 'value', '') == 'group':
group_info = await adapter.get_group_info(source.chat_id)
print('GROUP_INFO', group_info.model_dump())
members = await adapter.get_group_member_list(source.chat_id)
print('GROUP_MEMBER_LIST_COUNT', len(members))
await adapter.call_platform_api('send_chat_action', {'chat_id': source.chat_id, 'action': 'typing'})
count = await adapter.call_platform_api('get_chat_member_count', {'chat_id': source.chat_id})
print('GROUP_MEMBER_COUNT', count)
print('READY: click the callback button or react to a bot message if you want live callback/reaction events.')
await asyncio.sleep(max(5, timeout // 3))
finally:
await adapter.kill()
summary = {
'events': [summarize_event(event) for event in events],
'event_types': [event.type for event in events],
}
print('SUMMARY', json.dumps(summary, ensure_ascii=False))
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--token', default=os.getenv('TELEGRAM_BOT_TOKEN', ''))
parser.add_argument('--log', default='data/temp/live_telegram_eba_probe.jsonl')
parser.add_argument('--timeout', type=int, default=90)
args = parser.parse_args()
if not args.token:
raise SystemExit('Set TELEGRAM_BOT_TOKEN or pass --token')
log_path = Path(args.log)
if log_path.exists():
log_path.unlink()
asyncio.run(run_probe(args.token, log_path, args.timeout))
if __name__ == '__main__':
main()

View File

@@ -1,16 +1,25 @@
from __future__ import annotations
import base64
import datetime
import pathlib
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
import yaml
import telegram
from telegram.ext import CallbackQueryHandler, ChatMemberHandler, MessageHandler, MessageReactionHandler
from langbot.pkg.platform.adapters.telegram.event_converter import TelegramEventConverter
from langbot.pkg.platform.adapters.telegram.platform_api import PLATFORM_API_MAP
from langbot.pkg.platform.adapters.telegram.adapter import TelegramAdapter
from langbot.pkg.platform.botmgr import RuntimeBot
from langbot_plugin.api.definition.abstract.platform.event_logger import AbstractEventLogger
from langbot_plugin.api.entities.builtin.platform import entities as platform_entities
from langbot_plugin.api.entities.builtin.platform import events as platform_events
from langbot_plugin.api.entities.builtin.platform import message as platform_message
from langbot_plugin.api.entities import events as plugin_events
class DummyLogger(AbstractEventLogger):
@@ -38,6 +47,23 @@ def make_adapter() -> TelegramAdapter:
)
def make_update(data: dict) -> telegram.Update:
payload = {'update_id': 1000, **data}
return telegram.Update.de_json(payload, make_adapter().bot)
def base_message_payload(**overrides):
payload = {
'message_id': 10,
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
'chat': {'id': 123, 'type': 'private', 'first_name': 'Chat User'},
'from': {'id': 456, 'is_bot': False, 'first_name': 'Sender', 'username': 'sender'},
'text': 'hello',
}
payload.update(overrides)
return payload
def test_telegram_adapter_registers_all_declared_update_handlers():
adapter = make_adapter()
@@ -125,3 +151,290 @@ def test_telegram_supported_events_match_manifest():
assert adapter_events == manifest_events
assert 'message.deleted' not in adapter_events
assert 'group.info_updated' not in adapter_events
@pytest.mark.asyncio
async def test_telegram_converter_maps_message_and_edited_message_events():
update = make_update({'message': base_message_payload(text='hello @test_bot')})
event = await TelegramEventConverter.target2yiri(update, make_adapter().bot, 'test_bot')
assert isinstance(event, platform_events.MessageReceivedEvent)
assert event.type == 'message.received'
assert event.chat_type == platform_entities.ChatType.PRIVATE
assert event.chat_id == 123
assert event.sender.id == 456
assert platform_message.At in event.message_chain
assert isinstance(event.message_chain[0], platform_message.At)
assert isinstance(event.message_chain[1], platform_message.Plain)
assert event.message_chain[1].text == 'hello '
group_chat = {'id': -100123, 'type': 'supergroup', 'title': 'Test Group'}
edited_payload = base_message_payload(chat=group_chat, text='edited')
edited_payload['edit_date'] = edited_payload['date'] + 1
edited = make_update({'edited_message': edited_payload})
edited_event = await TelegramEventConverter.target2yiri(edited, make_adapter().bot, 'test_bot')
assert isinstance(edited_event, platform_events.MessageEditedEvent)
assert edited_event.type == 'message.edited'
assert edited_event.chat_type == platform_entities.ChatType.GROUP
assert edited_event.group.name == 'Test Group'
assert str(edited_event.new_content) == 'edited'
@pytest.mark.asyncio
async def test_telegram_converter_maps_non_message_updates():
chat_member = make_update(
{
'chat_member': {
'chat': {'id': -1001, 'type': 'supergroup', 'title': 'Group'},
'from': {'id': 1, 'is_bot': False, 'first_name': 'Admin'},
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
'old_chat_member': {
'user': {'id': 2, 'is_bot': False, 'first_name': 'Member'},
'status': 'left',
},
'new_chat_member': {
'user': {'id': 2, 'is_bot': False, 'first_name': 'Member'},
'status': 'member',
},
}
}
)
joined = await TelegramEventConverter.target2yiri(chat_member, make_adapter().bot, 'test_bot')
assert isinstance(joined, platform_events.MemberJoinedEvent)
assert joined.type == 'group.member_joined'
callback = make_update(
{
'callback_query': {
'id': 'cb-1',
'from': {'id': 3, 'is_bot': False, 'first_name': 'Clicker'},
'chat_instance': 'ci',
'data': 'button-data',
'message': base_message_payload(message_id=77),
}
}
)
callback_event = await TelegramEventConverter.target2yiri(callback, make_adapter().bot, 'test_bot')
assert isinstance(callback_event, platform_events.PlatformSpecificEvent)
assert callback_event.action == 'callback_query'
assert callback_event.data['callback_query_id'] == 'cb-1'
assert callback_event.data['data'] == 'button-data'
reaction = make_update(
{
'message_reaction': {
'chat': {'id': -1001, 'type': 'supergroup', 'title': 'Group'},
'message_id': 77,
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
'user': {'id': 3, 'is_bot': False, 'first_name': 'Reactor'},
'old_reaction': [],
'new_reaction': [{'type': 'emoji', 'emoji': '👍'}],
}
}
)
reaction_event = await TelegramEventConverter.target2yiri(reaction, make_adapter().bot, 'test_bot')
assert isinstance(reaction_event, platform_events.MessageReactionEvent)
assert reaction_event.reaction == '👍'
assert reaction_event.is_add is True
@pytest.mark.asyncio
async def test_telegram_converter_maps_bot_status_events():
base_member = {
'chat': {'id': -1001, 'type': 'supergroup', 'title': 'Group'},
'from': {'id': 1, 'is_bot': False, 'first_name': 'Admin'},
'date': int(datetime.datetime.now(datetime.UTC).timestamp()),
}
restricted_member = {
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
'status': 'restricted',
'is_member': True,
'can_send_messages': False,
'can_send_audios': False,
'can_send_documents': False,
'can_send_photos': False,
'can_send_videos': False,
'can_send_video_notes': False,
'can_send_voice_notes': False,
'can_send_polls': False,
'can_send_other_messages': False,
'can_add_web_page_previews': False,
'can_change_info': False,
'can_invite_users': False,
'can_pin_messages': False,
'can_manage_topics': False,
'until_date': 0,
}
invited = make_update(
{
'my_chat_member': {
**base_member,
'old_chat_member': {
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
'status': 'left',
},
'new_chat_member': {
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
'status': 'member',
},
}
}
)
invited_event = await TelegramEventConverter.target2yiri(invited, make_adapter().bot, 'test_bot')
assert isinstance(invited_event, platform_events.BotInvitedToGroupEvent)
muted = make_update(
{
'my_chat_member': {
**base_member,
'old_chat_member': {
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
'status': 'member',
},
'new_chat_member': {
**restricted_member,
},
}
}
)
muted_event = await TelegramEventConverter.target2yiri(muted, make_adapter().bot, 'test_bot')
assert isinstance(muted_event, platform_events.BotMutedEvent)
unmuted = make_update(
{
'my_chat_member': {
**base_member,
'old_chat_member': {
**restricted_member,
},
'new_chat_member': {
'user': {'id': 999, 'is_bot': True, 'first_name': 'Bot'},
'status': 'member',
},
}
}
)
unmuted_event = await TelegramEventConverter.target2yiri(unmuted, make_adapter().bot, 'test_bot')
assert isinstance(unmuted_event, platform_events.BotUnmutedEvent)
@pytest.mark.asyncio
async def test_telegram_reply_message_sends_text_image_and_file_components():
adapter = make_adapter()
bot = SimpleNamespace(
send_message=AsyncMock(),
send_photo=AsyncMock(),
send_document=AsyncMock(),
)
object.__setattr__(adapter, 'bot', bot)
update = make_update({'message': base_message_payload(message_id=88)})
message_source = platform_events.MessageReceivedEvent(
message_id=88,
source_platform_object=update,
)
await adapter.reply_message(
message_source,
platform_message.MessageChain(
[
platform_message.Plain(text='reply text'),
platform_message.Image(base64=base64.b64encode(b'image-bytes').decode('utf-8')),
platform_message.File(
name='test.txt',
size=4,
base64='data:text/plain;base64,' + base64.b64encode(b'test').decode('utf-8'),
),
]
),
quote_origin=True,
)
bot.send_message.assert_awaited_once()
bot.send_photo.assert_awaited_once()
bot.send_document.assert_awaited_once()
assert bot.send_message.await_args.kwargs['reply_to_message_id'] == 88
assert bot.send_photo.await_args.kwargs['reply_to_message_id'] == 88
assert bot.send_document.await_args.kwargs['document'].filename == 'test.txt'
@pytest.mark.asyncio
async def test_telegram_platform_apis_call_underlying_bot_methods():
bot = SimpleNamespace(
pin_chat_message=AsyncMock(),
unpin_chat_message=AsyncMock(),
unpin_all_chat_messages=AsyncMock(),
get_chat_administrators=AsyncMock(
return_value=[
SimpleNamespace(
user=SimpleNamespace(id=1, username='admin', first_name='Admin'),
status='administrator',
custom_title='Boss',
)
]
),
set_chat_title=AsyncMock(),
set_chat_description=AsyncMock(),
get_chat_member_count=AsyncMock(return_value=3),
send_chat_action=AsyncMock(),
create_chat_invite_link=AsyncMock(
return_value=SimpleNamespace(
invite_link='https://t.me/+abc',
name='invite',
is_primary=False,
is_revoked=False,
)
),
answer_callback_query=AsyncMock(),
)
assert await PLATFORM_API_MAP['pin_message'](bot, {'chat_id': 1, 'message_id': 2}) == {'ok': True}
assert await PLATFORM_API_MAP['unpin_message'](bot, {'chat_id': 1, 'message_id': 2}) == {'ok': True}
assert await PLATFORM_API_MAP['unpin_all_messages'](bot, {'chat_id': 1}) == {'ok': True}
admins = await PLATFORM_API_MAP['get_chat_administrators'](bot, {'chat_id': 1})
assert admins['administrators'][0]['user_id'] == 1
assert await PLATFORM_API_MAP['set_chat_title'](bot, {'chat_id': 1, 'title': 'New'}) == {'ok': True}
assert await PLATFORM_API_MAP['set_chat_description'](bot, {'chat_id': 1, 'description': 'Desc'}) == {'ok': True}
assert await PLATFORM_API_MAP['get_chat_member_count'](bot, {'chat_id': 1}) == {'count': 3}
assert await PLATFORM_API_MAP['send_chat_action'](bot, {'chat_id': 1, 'action': 'typing'}) == {'ok': True}
invite = await PLATFORM_API_MAP['create_chat_invite_link'](bot, {'chat_id': 1, 'name': 'invite'})
assert invite['invite_link'] == 'https://t.me/+abc'
assert await PLATFORM_API_MAP['answer_callback_query'](bot, {'callback_query_id': 'cb'}) == {'ok': True}
def test_runtime_bot_maps_telegram_eba_events_to_plugin_events():
group = platform_entities.UserGroup(id='group-1', name='Group')
user = platform_entities.User(id='user-1', nickname='User')
cases = [
(
platform_events.MessageReceivedEvent(
message_id='msg',
message_chain=platform_message.MessageChain([platform_message.Plain(text='hi')]),
sender=user,
chat_id='user-1',
),
plugin_events.MessageReceived,
),
(
platform_events.MessageReactionEvent(message_id='msg', user=user, reaction='👍'),
plugin_events.MessageReactionReceived,
),
(
platform_events.MemberJoinedEvent(group=group, member=user),
plugin_events.GroupMemberJoined,
),
(
platform_events.BotUnmutedEvent(group=group, operator=user),
plugin_events.BotUnmuted,
),
(
platform_events.PlatformSpecificEvent(adapter_name='telegram', action='callback_query', data={'data': 'x'}),
plugin_events.PlatformSpecificEventReceived,
),
]
for platform_event, plugin_event_type in cases:
plugin_event = RuntimeBot._eba_event_to_plugin_event(platform_event)
assert isinstance(plugin_event, plugin_event_type)
assert plugin_event.model_dump()['event_name'] == plugin_event_type.__name__