Files
LangBot/tests/unit_tests/platform/test_kook_eba_adapter.py
2026-06-04 18:30:18 +08:00

276 lines
9.5 KiB
Python

from __future__ import annotations
import pathlib
from unittest.mock import AsyncMock
import pytest
import yaml
from langbot.pkg.platform.adapters.kook.adapter import KookAdapter
from langbot.pkg.platform.adapters.kook.event_converter import KookEventConverter
from langbot.pkg.platform.adapters.kook.message_converter import KookMessageConverter
from langbot.pkg.platform.adapters.kook.platform_api import PLATFORM_API_MAP, get_gateway
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
class DummyLogger(AbstractEventLogger):
async def info(self, text, images=None, message_session_id=None, no_throw=True):
pass
async def debug(self, text, images=None, message_session_id=None, no_throw=True):
pass
async def warning(self, text, images=None, message_session_id=None, no_throw=True):
pass
async def error(self, text, images=None, message_session_id=None, no_throw=True):
pass
def make_adapter() -> KookAdapter:
return KookAdapter({'token': 'fake', 'enable-stream-reply': False}, DummyLogger())
def manifest() -> dict:
manifest_path = (
pathlib.Path(__file__).parents[3]
/ 'src'
/ 'langbot'
/ 'pkg'
/ 'platform'
/ 'adapters'
/ 'kook'
/ 'manifest.yaml'
)
return yaml.safe_load(manifest_path.read_text())
def fake_kook_message(**overrides):
event = {
'channel_type': 'GROUP',
'type': 9,
'author_id': 'u1',
'target_id': 'c1',
'msg_id': 'm1',
'msg_timestamp': 1_775_000_000_000,
'content': 'hi (met)u2(met) and (met)all(met)',
'extra': {
'channel_name': 'general',
'guild_id': 'g1',
'guild_name': 'Guild',
'author': {
'id': 'u1',
'username': 'alice',
'nickname': 'Alice',
'avatar': 'https://example/avatar.png',
},
'mention': ['u2'],
'mention_all': True,
},
}
event.update(overrides)
return event
def test_kook_supported_events_match_manifest():
assert make_adapter().get_supported_events() == manifest()['spec']['supported_events']
def test_kook_supported_apis_match_manifest():
supported_apis = make_adapter().get_supported_apis()
manifest_apis = manifest()['spec']['supported_apis']
assert supported_apis == manifest_apis['required'] + manifest_apis['optional']
def test_kook_platform_api_map_matches_manifest():
manifest_actions = {item['action'] for item in manifest()['spec']['platform_specific_apis']}
assert set(PLATFORM_API_MAP) == manifest_actions
@pytest.mark.asyncio
async def test_kook_adapter_dispatches_most_specific_eba_listener():
adapter = make_adapter()
calls: list[str] = []
async def event_listener(event, adapter):
calls.append('event')
async def eba_listener(event, adapter):
calls.append('eba')
async def message_listener(event, adapter):
calls.append('message')
adapter.register_listener(platform_events.Event, event_listener)
adapter.register_listener(platform_events.EBAEvent, eba_listener)
adapter.register_listener(platform_events.MessageReceivedEvent, message_listener)
event = platform_events.MessageReceivedEvent(
message_id='m1',
message_chain=platform_message.MessageChain([platform_message.Plain(text='hello')]),
sender=platform_entities.User(id='u1'),
chat_id='c1',
)
await adapter._dispatch_eba_event(event)
assert calls == ['message']
@pytest.mark.asyncio
async def test_kook_message_converter_maps_target_text_mentions_and_source():
chain = await KookMessageConverter.target2yiri(fake_kook_message(), bot_account_id='bot')
assert isinstance(chain[0], platform_message.Source)
assert chain[0].id == 'm1'
assert isinstance(chain[1], platform_message.Plain)
assert chain[1].text == 'hi '
assert isinstance(chain[2], platform_message.At)
assert chain[2].target == 'u2'
assert isinstance(chain[4], platform_message.AtAll)
@pytest.mark.asyncio
async def test_kook_message_converter_maps_media_components():
image = await KookMessageConverter.target2yiri(fake_kook_message(type=2, content='https://example/image.png'))
assert isinstance(image[1], platform_message.Image)
assert image[1].url == 'https://example/image.png'
file_chain = await KookMessageConverter.target2yiri(
fake_kook_message(type=4, content='https://example/file.bin', extra={'attachments': {'name': 'file.bin'}})
)
assert isinstance(file_chain[1], platform_message.File)
assert file_chain[1].name == 'file.bin'
voice = await KookMessageConverter.target2yiri(fake_kook_message(type=8, content='https://example/voice.mp3'))
assert isinstance(voice[1], platform_message.Voice)
@pytest.mark.asyncio
async def test_kook_message_converter_maps_common_components_to_target():
content, msg_type = await KookMessageConverter.yiri2target(
platform_message.MessageChain(
[
platform_message.Plain(text='hi '),
platform_message.At(target='u1'),
platform_message.Plain(text=' all '),
platform_message.AtAll(),
]
)
)
assert content == 'hi (met)u1(met) all (met)all(met)'
assert msg_type == 1
@pytest.mark.asyncio
async def test_kook_event_converter_maps_group_private_and_platform_specific_events():
group_event = await KookEventConverter.target2yiri(fake_kook_message(), bot_account_id='bot')
assert isinstance(group_event, platform_events.MessageReceivedEvent)
assert group_event.type == 'message.received'
assert group_event.adapter_name == 'kook'
assert group_event.chat_type == platform_entities.ChatType.GROUP
assert group_event.chat_id == 'c1'
assert group_event.group.id == 'c1'
assert group_event.sender.id == 'u1'
private_event = await KookEventConverter.target2yiri(
fake_kook_message(channel_type='PERSON', target_id='u1', extra={'code': 'chat-code'}),
bot_account_id='bot',
)
assert private_event.chat_type == platform_entities.ChatType.PRIVATE
assert private_event.chat_id == 'chat-code'
assert private_event.group is None
specific = await KookEventConverter.target2yiri({'type': 255, 'target_id': 'raw'}, bot_account_id='bot')
assert isinstance(specific, platform_events.PlatformSpecificEvent)
assert specific.action == '255'
@pytest.mark.asyncio
async def test_kook_event_converter_maps_legacy_events():
legacy_group = await KookEventConverter.target2legacy(fake_kook_message(), bot_account_id='bot')
assert isinstance(legacy_group, platform_events.GroupMessage)
assert legacy_group.sender.group.id == 'c1'
legacy_private = await KookEventConverter.target2legacy(
fake_kook_message(channel_type='PERSON', target_id='u1'),
bot_account_id='bot',
)
assert isinstance(legacy_private, platform_events.FriendMessage)
@pytest.mark.asyncio
async def test_kook_send_and_reply_pass_expected_payloads():
adapter = make_adapter()
adapter._request = AsyncMock(return_value={'code': 0, 'data': {'msg_id': 'sent'}})
result = await adapter.send_message(
'group',
'c1',
platform_message.MessageChain([platform_message.Plain(text='hello')]),
)
assert result.message_id == 'sent'
adapter._request.assert_awaited_with(
'POST',
'/message/create',
json={'target_id': 'c1', 'content': 'hello', 'type': 1},
)
source = await KookEventConverter.target2yiri(fake_kook_message(), bot_account_id='bot')
await adapter.reply_message(source, platform_message.MessageChain([platform_message.Plain(text='reply')]), True)
assert adapter._request.await_args_list[-1].args == ('POST', '/message/create')
payload = adapter._request.await_args_list[-1].kwargs['json']
assert payload['reply_msg_id'] == 'm1'
assert payload['quote'] == 'm1'
assert payload['content'] == 'reply'
@pytest.mark.asyncio
async def test_kook_get_gateway_redacts_token_in_platform_api_result():
adapter = make_adapter()
adapter._request = AsyncMock(
return_value={
'code': 0,
'data': {
'url': 'wss://example.invalid/gateway?compress=1&token=secret-token',
},
}
)
result = await get_gateway(adapter, {'compress': 1})
assert result['data']['url'] == 'wss://example.invalid/gateway?compress=1&token=%3Credacted%3E'
assert 'secret-token' not in result['data']['url']
@pytest.mark.asyncio
async def test_kook_handle_event_dispatches_eba_and_legacy_then_caches():
adapter = make_adapter()
adapter.bot_account_id = 'bot'
calls: list[str] = []
async def legacy_listener(event, adapter):
calls.append(type(event).__name__)
async def eba_listener(event, adapter):
calls.append(event.type)
adapter.register_listener(platform_events.GroupMessage, legacy_listener)
adapter.register_listener(platform_events.MessageReceivedEvent, eba_listener)
await adapter._handle_event(fake_kook_message(), 7)
assert calls == ['GroupMessage', 'message.received']
assert adapter.current_sn == 7
assert 'm1' in adapter._message_cache
assert 'u1' in adapter._user_cache
assert 'c1' in adapter._group_cache