mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
fix(telegram): resolve real-client event and media delivery regressions
This commit is contained in:
@@ -60,6 +60,18 @@ class TestQueryToEventEnvelope:
|
||||
|
||||
assert event.conversation_id == 'conv-uuid-123'
|
||||
|
||||
@pytest.mark.parametrize('launcher_id', [123456789, -100123456789, 'launcher-123'])
|
||||
def test_query_to_event_accepts_platform_launcher_ids(self, mock_query, launcher_id):
|
||||
"""Telegram private and group IDs are numeric at the Query boundary."""
|
||||
mock_query.launcher_id = launcher_id
|
||||
mock_query.session.using_conversation.uuid = None
|
||||
|
||||
event = QueryEntryAdapter.query_to_event(mock_query)
|
||||
context = QueryEntryAdapter._build_conversation_context(mock_query)
|
||||
|
||||
assert context.launcher_id == str(launcher_id)
|
||||
assert event.conversation_id == f'person_{launcher_id}'
|
||||
|
||||
def test_query_to_event_prefers_variable_conversation_id_when_conversation_uuid_missing(self, mock_query):
|
||||
"""Pipeline variables can provide the conversation identity for state scope."""
|
||||
mock_query.session.using_conversation.uuid = None
|
||||
|
||||
@@ -402,7 +402,10 @@ async def test_telegram_converter_maps_non_message_updates():
|
||||
}
|
||||
}
|
||||
)
|
||||
reaction_event = await TelegramEventConverter.target2yiri(reaction, make_adapter().bot, 'test_bot')
|
||||
adapter = make_adapter()
|
||||
reaction_handler = next(h for h in adapter.application.handlers[0] if isinstance(h, MessageReactionHandler))
|
||||
assert reaction_handler.check_update(reaction) is True
|
||||
reaction_event = await TelegramEventConverter.target2yiri(reaction, adapter.bot, 'test_bot')
|
||||
assert isinstance(reaction_event, platform_events.MessageReactionEvent)
|
||||
assert reaction_event.reaction == '👍'
|
||||
assert reaction_event.is_add is True
|
||||
@@ -471,6 +474,7 @@ async def test_telegram_converter_maps_bot_status_events():
|
||||
)
|
||||
muted_event = await TelegramEventConverter.target2yiri(muted, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(muted_event, platform_events.BotMutedEvent)
|
||||
assert muted_event.duration is None
|
||||
|
||||
unmuted = make_update(
|
||||
{
|
||||
@@ -489,6 +493,30 @@ async def test_telegram_converter_maps_bot_status_events():
|
||||
unmuted_event = await TelegramEventConverter.target2yiri(unmuted, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(unmuted_event, platform_events.BotUnmutedEvent)
|
||||
|
||||
partial_unmute = make_update(
|
||||
{
|
||||
'my_chat_member': {
|
||||
**base_member,
|
||||
'old_chat_member': restricted_member,
|
||||
'new_chat_member': {**restricted_member, 'can_send_messages': True},
|
||||
}
|
||||
}
|
||||
)
|
||||
partial_event = await TelegramEventConverter.target2yiri(partial_unmute, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(partial_event, platform_events.BotUnmutedEvent)
|
||||
|
||||
removed = make_update(
|
||||
{
|
||||
'my_chat_member': {
|
||||
**base_member,
|
||||
'old_chat_member': restricted_member,
|
||||
'new_chat_member': {'user': restricted_member['user'], 'status': 'kicked', 'until_date': 0},
|
||||
}
|
||||
}
|
||||
)
|
||||
removed_event = await TelegramEventConverter.target2yiri(removed, make_adapter().bot, 'test_bot')
|
||||
assert isinstance(removed_event, platform_events.BotRemovedFromGroupEvent)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_telegram_reply_message_sends_text_image_and_file_components():
|
||||
@@ -627,3 +655,152 @@ def test_runtime_bot_maps_telegram_eba_events_to_plugin_events():
|
||||
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__
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('with_text', [True, False])
|
||||
async def test_stream_completion_delivers_attachments_once(with_text):
|
||||
adapter = make_adapter()
|
||||
bot = SimpleNamespace(
|
||||
send_message=AsyncMock(return_value=SimpleNamespace(message_id=900)),
|
||||
edit_message_text=AsyncMock(),
|
||||
send_photo=AsyncMock(return_value=SimpleNamespace(message_id=901)),
|
||||
send_document=AsyncMock(return_value=SimpleNamespace(message_id=902)),
|
||||
)
|
||||
object.__setattr__(adapter, 'bot', bot)
|
||||
adapter.msg_stream_id['stream'] = ('message', 999, True)
|
||||
source = platform_events.MessageReceivedEvent(
|
||||
message_id=88, source_platform_object=make_update({'message': base_message_payload(message_id=88)})
|
||||
)
|
||||
parts = [platform_message.Plain(text='done')] if with_text else []
|
||||
parts.extend(
|
||||
[
|
||||
platform_message.Image(base64='data:image/png;base64,' + base64.b64encode(b'image').decode()),
|
||||
platform_message.File(
|
||||
name='result.txt', base64='data:text/plain;base64,' + base64.b64encode(b'result').decode()
|
||||
),
|
||||
]
|
||||
)
|
||||
chunk = SimpleNamespace(resp_message_id='stream', msg_sequence=2, tool_calls=None)
|
||||
chain = platform_message.MessageChain(parts)
|
||||
await adapter.reply_message_chunk(source, chunk, chain, is_final=False)
|
||||
bot.send_photo.assert_not_awaited()
|
||||
bot.send_document.assert_not_awaited()
|
||||
await adapter.reply_message_chunk(source, chunk, chain, quote_origin=True, is_final=True)
|
||||
await adapter.reply_message_chunk(source, chunk, chain, quote_origin=True, is_final=True)
|
||||
bot.send_photo.assert_awaited_once()
|
||||
bot.send_document.assert_awaited_once()
|
||||
assert bot.send_document.await_args.kwargs['document'].filename == 'result.txt'
|
||||
assert bot.send_photo.await_args.kwargs['reply_to_message_id'] == 88
|
||||
assert 'stream' not in adapter.msg_stream_id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('reply', [False, True])
|
||||
async def test_telegram_send_returns_message_result(reply):
|
||||
adapter = make_adapter()
|
||||
bot = SimpleNamespace(send_message=AsyncMock(return_value=SimpleNamespace(message_id=901)))
|
||||
object.__setattr__(adapter, 'bot', bot)
|
||||
chain = platform_message.MessageChain([platform_message.Plain(text='hello')])
|
||||
if reply:
|
||||
source = platform_events.MessageReceivedEvent(
|
||||
message_id=88, source_platform_object=make_update({'message': base_message_payload(message_id=88)})
|
||||
)
|
||||
result = await adapter.reply_message(source, chain, quote_origin=True)
|
||||
assert bot.send_message.await_args.kwargs['reply_to_message_id'] == 88
|
||||
else:
|
||||
result = await adapter.send_message('group', '-100123#7', chain)
|
||||
assert bot.send_message.await_args.kwargs['message_thread_id'] == 7
|
||||
assert isinstance(result, platform_events.MessageResult)
|
||||
assert result.message_id == 901
|
||||
assert result.raw == {'message_ids': [901]}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('until, expected', [(0, None), (1060, 60), (900, 0)])
|
||||
def test_restriction_duration_uses_event_time(until, expected):
|
||||
from langbot.pkg.platform.adapters.telegram.event_converter import _restriction_duration
|
||||
|
||||
assert (
|
||||
_restriction_duration(
|
||||
datetime.datetime.fromtimestamp(until, datetime.UTC),
|
||||
datetime.datetime.fromtimestamp(1000, datetime.UTC),
|
||||
)
|
||||
== expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('new_reaction', [[], [{'type': 'emoji', 'emoji': '❤'}]])
|
||||
async def test_reaction_removal_keeps_removed_emoji(new_reaction):
|
||||
update = make_update(
|
||||
{
|
||||
'message_reaction': {
|
||||
'chat': {'id': -1001, 'type': 'supergroup', 'title': 'Group'},
|
||||
'message_id': 77,
|
||||
'date': 1000,
|
||||
'user': {'id': 3, 'is_bot': False, 'first_name': 'Reactor'},
|
||||
'old_reaction': [{'type': 'emoji', 'emoji': '👍'}, *new_reaction],
|
||||
'new_reaction': new_reaction,
|
||||
}
|
||||
}
|
||||
)
|
||||
event = await TelegramEventConverter.target2yiri(update, make_adapter().bot, 'test_bot')
|
||||
assert event.reaction == '👍'
|
||||
assert event.is_add is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('reply', [False, True])
|
||||
async def test_rejected_photo_falls_back_without_resending_other_attachments(reply):
|
||||
adapter = make_adapter()
|
||||
bot = SimpleNamespace(
|
||||
send_photo=AsyncMock(side_effect=telegram.error.BadRequest('Image_process_failed')),
|
||||
send_document=AsyncMock(return_value=SimpleNamespace(message_id=902)),
|
||||
)
|
||||
object.__setattr__(adapter, 'bot', bot)
|
||||
chain = platform_message.MessageChain(
|
||||
[
|
||||
platform_message.File(name='result.txt', base64='data:text/plain;base64,cmVzdWx0'),
|
||||
platform_message.Image(base64='data:image/png;base64,aW1hZ2U='),
|
||||
]
|
||||
)
|
||||
if reply:
|
||||
source = platform_events.MessageReceivedEvent(
|
||||
message_id=88, source_platform_object=make_update({'message': base_message_payload(message_id=88)})
|
||||
)
|
||||
result = await adapter.reply_message(source, chain, quote_origin=True)
|
||||
else:
|
||||
result = await adapter.send_message('private', '123', chain)
|
||||
assert result.message_id == 902
|
||||
assert bot.send_document.await_count == 2
|
||||
assert [call.kwargs['document'].filename for call in bot.send_document.await_args_list] == ['result.txt', 'image']
|
||||
assert bot.send_document.await_args.kwargs['document'].input_file_content == b'image'
|
||||
if reply:
|
||||
assert bot.send_document.await_args.kwargs['reply_to_message_id'] == 88
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attachment_only_completion_removes_thinking_placeholder():
|
||||
adapter = make_adapter()
|
||||
object.__setattr__(
|
||||
adapter,
|
||||
'bot',
|
||||
SimpleNamespace(
|
||||
send_document=AsyncMock(return_value=SimpleNamespace(message_id=902)),
|
||||
delete_message=AsyncMock(),
|
||||
),
|
||||
)
|
||||
adapter.msg_stream_id['stream'] = ('message', 999, False)
|
||||
source = platform_events.MessageReceivedEvent(
|
||||
message_id=88, source_platform_object=make_update({'message': base_message_payload(message_id=88)})
|
||||
)
|
||||
await adapter.reply_message_chunk(
|
||||
source,
|
||||
SimpleNamespace(resp_message_id='stream', msg_sequence=1, tool_calls=None),
|
||||
platform_message.MessageChain(
|
||||
[platform_message.File(name='result.txt', base64='data:text/plain;base64,cmVzdWx0')]
|
||||
),
|
||||
is_final=True,
|
||||
)
|
||||
adapter.bot.delete_message.assert_awaited_once_with(chat_id=123, message_id=999)
|
||||
assert 'stream' not in adapter.msg_stream_id
|
||||
|
||||
Reference in New Issue
Block a user