mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-15 00:46:07 +00:00
0cde4c71cd
- Added support for select fields in Telegram adapter, including option extraction and callback handling. - Implemented form action processing for Telegram callbacks, improving user interaction feedback. - Introduced new helper functions for building keyboards and resolving select button actions in QQ Official adapter. - Enhanced DifyServiceAPIRunner to handle cumulative streaming responses and improve error handling during workflow resumes. - Added unit tests for new functionalities in Telegram and QQ Official adapters, ensuring robust behavior for select fields and form actions.
177 lines
5.5 KiB
Python
177 lines
5.5 KiB
Python
"""Tests for DingTalk adapter helper behavior."""
|
||
|
||
import asyncio
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from langbot.pkg.platform.sources.dingtalk import (
|
||
DingTalkAdapter,
|
||
_dingtalk_clean_form_content,
|
||
_dingtalk_completed_input_lines,
|
||
_dingtalk_extract_component_inputs,
|
||
_dingtalk_form_component_params,
|
||
_dingtalk_pending_input_defs,
|
||
)
|
||
|
||
|
||
def test_dingtalk_select_component_params_expose_options():
|
||
params = _dingtalk_form_component_params(
|
||
{
|
||
'_current_input_field': 'choice',
|
||
'input_defs': [
|
||
{
|
||
'output_variable_name': 'choice',
|
||
'type': 'select',
|
||
'option_source': {'type': 'constant', 'value': ['A', 'B']},
|
||
}
|
||
],
|
||
'inputs': {},
|
||
}
|
||
)
|
||
|
||
assert params['select_visible'] == 'true'
|
||
assert params['select_placeholder'] == 'choice'
|
||
assert params['select_options'] == ['A', 'B']
|
||
assert [option['value'] for option in params['index_o']] == ['A', 'B']
|
||
assert [option['value'] for option in params['test_index']] == ['A', 'B']
|
||
assert params['index_o'][0]['text']['zh_CN'] == 'A'
|
||
assert params['index_o'][0]['text']['en_US'] == 'A'
|
||
assert params['select_index'] == -1
|
||
|
||
|
||
def test_dingtalk_extract_select_from_builtin_result_dict():
|
||
inputs = _dingtalk_extract_component_inputs({'selectResult': {'index': 1, 'value': 'B'}})
|
||
|
||
assert inputs == {'select': 'B'}
|
||
|
||
|
||
def test_dingtalk_extract_select_from_template_param_string():
|
||
inputs = _dingtalk_extract_component_inputs({'select': '{"index": 1, "value": "B"}'})
|
||
|
||
assert inputs == {'select': '{"index": 1, "value": "B"}'}
|
||
|
||
|
||
def test_dingtalk_extract_input_and_select_together():
|
||
inputs = _dingtalk_extract_component_inputs(
|
||
{
|
||
'inputResult': {'value': 'looks good'},
|
||
'__built_in_selectResult__': {'index': 0, 'value': 'A'},
|
||
}
|
||
)
|
||
|
||
assert inputs == {'input': 'looks good', 'select': 'A'}
|
||
|
||
|
||
def test_dingtalk_pending_input_defs_includes_file_fields():
|
||
pending = _dingtalk_pending_input_defs(
|
||
{
|
||
'input_defs': [
|
||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||
{'output_variable_name': 'files', 'type': 'file-list'},
|
||
],
|
||
'inputs': {'comment': 'ready'},
|
||
}
|
||
)
|
||
|
||
assert [field['output_variable_name'] for field in pending] == ['files']
|
||
|
||
|
||
def test_dingtalk_completed_input_lines_include_text_and_select_values():
|
||
lines = _dingtalk_completed_input_lines(
|
||
{
|
||
'all_input_defs': [
|
||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||
{
|
||
'output_variable_name': 'choice',
|
||
'type': 'select',
|
||
'option_source': {'type': 'constant', 'value': ['A', 'B']},
|
||
},
|
||
],
|
||
'inputs': {'comment': 'looks good', 'choice': 'B'},
|
||
}
|
||
)
|
||
|
||
assert lines == ['✅ 已填写 comment:**looks good**', '✅ 已选择 choice:**B**']
|
||
|
||
|
||
def test_dingtalk_clean_form_content_uses_all_input_defs():
|
||
content = _dingtalk_clean_form_content(
|
||
{
|
||
'raw_form_content': 'Hello\n\n{{#$output.comment#}}\n\n{{#$output.choice#}}\n',
|
||
'input_defs': [],
|
||
'all_input_defs': [
|
||
{'output_variable_name': 'comment', 'type': 'paragraph'},
|
||
{'output_variable_name': 'choice', 'type': 'select'},
|
||
],
|
||
}
|
||
)
|
||
|
||
assert content == 'Hello'
|
||
|
||
|
||
def _build_card_action_adapter() -> DingTalkAdapter:
|
||
adapter = DingTalkAdapter.model_construct(
|
||
card_state={
|
||
'card-1': {
|
||
'session_key': 'group_group-1',
|
||
'launcher_type': 'group',
|
||
'launcher_id': 'group-1',
|
||
'sender_user_id': 'initiator-1',
|
||
'form_token': 'token-1',
|
||
'workflow_run_id': 'run-1',
|
||
'actions': [{'id': 'approve', 'title': 'Approve'}],
|
||
'node_title': 'Review',
|
||
'form_content': 'Please review',
|
||
'input_defs': [],
|
||
'inputs': {},
|
||
}
|
||
},
|
||
active_turn_card={},
|
||
active_turn_text={},
|
||
)
|
||
adapter.logger = AsyncMock()
|
||
adapter.ap = MagicMock()
|
||
adapter.ap.platform_mgr.bots = []
|
||
adapter.ap.query_pool.add_query = AsyncMock()
|
||
return adapter
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_dingtalk_group_card_action_uses_clicker_as_sender():
|
||
adapter = _build_card_action_adapter()
|
||
|
||
with patch.object(DingTalkAdapter, '_mark_card_resolved', new=AsyncMock()) as mark_resolved:
|
||
await adapter._on_card_action(
|
||
{
|
||
'out_track_id': 'card-1',
|
||
'user_id': 'reviewer-2',
|
||
'action_id': 'approve',
|
||
'params': {},
|
||
}
|
||
)
|
||
await asyncio.sleep(0)
|
||
|
||
call = adapter.ap.query_pool.add_query.await_args
|
||
assert call.kwargs['launcher_id'] == 'group-1'
|
||
assert call.kwargs['sender_id'] == 'reviewer-2'
|
||
assert call.kwargs['message_event'].sender.id == 'reviewer-2'
|
||
mark_resolved.assert_awaited_once()
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_dingtalk_unknown_card_action_is_rejected():
|
||
adapter = _build_card_action_adapter()
|
||
|
||
await adapter._on_card_action(
|
||
{
|
||
'out_track_id': 'card-1',
|
||
'user_id': 'reviewer-2',
|
||
'action_id': 'not-on-card',
|
||
'params': {},
|
||
}
|
||
)
|
||
|
||
adapter.ap.query_pool.add_query.assert_not_awaited()
|
||
adapter.logger.warning.assert_awaited_once()
|