Files
LangBot/tests/unit_tests/platform/test_dingtalk_adapter.py
T
Dongchuan Fu 0755beebcd feat: add supports for dify hitl (#2226)
* feat: Implement workflow form handling for paused workflows

- Added module-level storage for pending forms to manage state across sessions.
- Introduced functions to set, get, and clear pending forms with expiration handling.
- Enhanced DifyServiceAPIRunner to support resuming paused workflows via form actions.
- Implemented logic to yield human input requests and display appropriate messages.
- Updated workflow submission methods to handle paused states and resume actions.
- Ensured proper merging of pending form actions with user inputs for seamless interaction.

* feat: Add '_routed_by_rule' variable to form action in Lark and Telegram adapters

* feat: Enhance Lark and Telegram adapters with new form handling for paused workflows

* feat: Enhance TelegramAdapter to handle form action buttons and message threading

* feat: Improve TelegramAdapter message handling with enhanced error management and draft message support

* feat: Add the function for formatting human input text to support adapters without rich UI.

* feat(dingtalk): implement human input card support and card action handling

- Add a new module `card_callback.py` to handle card action button clicks from DingTalk.
- Introduce `DingTalkCardActionHandler` to process card action callbacks and extract parameters.
- Update `DingTalkAdapter` to manage card state and handle form input through a single card template.
- Add configuration for `human_input_card_template_id` in `dingtalk.yaml` to specify the template for human input.
- Create a new card template `dingtalk_human_input_card.json` for rendering human input prompts and buttons.

* feat(dingtalk): enhance human input card functionality with streaming support and active turn management

- Updated the DingTalk card template to enable streaming mode and multi-update configuration.
- Removed the obsolete delete_card method from DingTalkClient to streamline card management.
- Enhanced DingTalkAdapter to manage active turn cards and accumulated streaming text, ensuring a seamless user experience during human input prompts.
- Modified the create_message_card method to utilize existing active cards for resumed workflows, preventing duplication.
- Improved the _paint_form_on_card method to update existing cards with human input prompts and buttons dynamically.
- Updated the dingtalk_human_input_card.json template to reflect the new streaming capabilities and configuration options.

* feat(wecom): implement Dify human input pause handling with button interaction support

* feat(qqofficial): implement Dify human input button interaction handling and markdown keyboard support

* feat(qqofficial): implement one-click QR binding and enhance localization support

* feat(discord): implement Discord form view with button interactions for Dify actions

* fix(telegram): correct group chat type check and handle oversized callback data for Telegram actions
fix(difysvapi): ensure safe access to remove-think configuration in pipeline settings

* feat(dify): add support for chatflow app type and enhance human input handling

* feat(telegram): add action title feedback for user selections in Telegram messages

* feat(lark): enhance LarkAdapter to store form content for resume notices

* feat(dingtalk): update display formatting for card content with HTML line breaks

* feat(dingtalk): add feedback functionality to cards with 👍/👎 buttons

- Implemented feedback state management for cards, allowing users to provide feedback via thumbs up/down buttons.
- Enhanced card rendering to include feedback buttons when appropriate.
- Registered feedback listeners to handle feedback events and update card states accordingly.
- Updated the card template to support dynamic button rendering for feedback actions.
- Improved error handling and logging for feedback actions and card updates.

* fix: add Avatar component to dingtalk_human_input_card.json for enhanced user interaction

* feat(wecom): add optional source block to interactive template cards for enhanced branding

* feat(wecom): add functions for template card action extraction and update, enhance button interaction handling

* feat(qqofficial): synchronize passive-reply counter with inbound message sequence

* feat(qqofficial): add method to identify invisible form placeholder chunks in messages

* feat(dingtalk): add download link for human input card template and enhance dynamic form configuration

* feat(telegram): enhance message handling with group stream deletion and form placeholder detection

* Add unit tests for DingTalk, Lark, WeComBot, and Dify service API runners

- Implement tests for DingTalk adapter helper functions including form content cleaning, input extraction, and completed input lines.
- Create unit tests for Lark adapter helper functions focusing on input extraction and completed input lines.
- Add tests for WeComBot template card functionalities, including event extraction and payload building for human input.
- Enhance Dify service API runner tests to cover human input forms, including input collection, action handling, and form snapshot extraction.

* feat: Enhance Telegram and QQ Official adapters with select field handling and form action processing

- 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.

* feat(lark): add functions for current input definitions and visible form content handling
feat(qqofficial): update fallback text handling for non-streaming scenarios
feat(difysvapi): enhance form content processing for interactive fields and actions
test: add unit tests for Lark and QQ Official adapter functionalities

* Add tests for DingTalk adapter content processing and markdown formatting

- Updated the assertion in `test_dingtalk_completed_input_lines_include_text_and_select_values` to remove unnecessary markdown formatting.
- Added new tests to verify that `_dingtalk_clean_form_content` maintains the order of prompts and completed values in various scenarios.
- Introduced `test_dingtalk_card_markdown_preserves_internal_line_breaks` to ensure internal line breaks are correctly converted to HTML line breaks.

* feat: Refactor input handling and feedback messages across multiple adapters

* feat: Update the human-computer interaction template cards, and optimize the prompt information and content display.

* feat: Refactor pending form handling to isolate by bot and pipeline

* feat: Enhance error handling and caching for Dify and WeCom interactions

* feat: Enhance select input handling and validation in Dify API runner and Telegram adapter

* feat: Add missing completed input lines handling in DingTalk adapter

* feat: Add pipeline_uuid handling across multiple adapters and update related tests
2026-07-13 00:42:46 +08:00

255 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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_card_markdown,
_dingtalk_clean_form_content,
_dingtalk_completed_input_lines,
_dingtalk_extract_component_inputs,
_dingtalk_form_component_params,
_dingtalk_missing_completed_input_lines,
_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_extract_component_inputs_strips_card_line_endings():
inputs = _dingtalk_extract_component_inputs(
{
'inputResult': {'value': '回复我测试\r\n'},
'selectResult': {'value': '1\r'},
}
)
assert inputs == {'input': '回复我测试', 'select': '1'}
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 == ['✅ commentlooks good', '✅ choiceB']
def test_dingtalk_completed_inputs_are_not_repeated_when_already_interleaved():
form_data = {
'all_input_defs': [
{'output_variable_name': 'us_input', 'type': 'paragraph'},
{'output_variable_name': 'xiala', 'type': 'select'},
],
'inputs': {'us_input': '回复我测试\r', 'xiala': '1'},
}
form_content = '你好\n请输入你的问题\n✅ us_input:回复我测试\n请选择你的答案\n✅ xiala1'
assert _dingtalk_missing_completed_input_lines(form_data, form_content) == []
def test_dingtalk_completed_inputs_are_appended_when_template_does_not_render_them():
form_data = {
'all_input_defs': [{'output_variable_name': 'comment', 'type': 'paragraph'}],
'inputs': {'comment': 'ready'},
}
assert _dingtalk_missing_completed_input_lines(form_data, 'Please review') == ['✅ commentready']
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 test_dingtalk_field_stage_keeps_prior_prompts_and_completed_values():
content = _dingtalk_clean_form_content(
{
'_current_input_field': 'choice',
'raw_form_content': ('Question\n{{#$output.comment#}}\nChoose an answer\n{{#$output.choice#}}'),
'form_content': 'Choose an answer',
'input_defs': [
{'output_variable_name': 'comment', 'type': 'paragraph'},
{'output_variable_name': 'choice', 'type': 'select'},
],
'inputs': {'comment': 'hello'},
}
)
assert '{{#$output.' not in content
assert content.index('Question') < content.index('comment')
assert content.index('comment') < content.index('Choose an answer')
def test_dingtalk_final_action_stage_interleaves_prompts_and_completed_values():
content = _dingtalk_clean_form_content(
{
'_action_select_only': True,
'raw_form_content': ('11\nQuestion\n{{#$output.comment#}}\nChoose an answer\n{{#$output.choice#}}'),
'all_input_defs': [
{'output_variable_name': 'comment', 'type': 'paragraph'},
{'output_variable_name': 'choice', 'type': 'select'},
],
'inputs': {'comment': 'hello', 'choice': 'B'},
}
)
assert '{{#$output.' not in content
assert content.startswith('11\nQuestion')
assert content.index('Question') < content.index('comment')
assert content.index('comment') < content.index('Choose an answer')
assert content.index('Choose an answer') < content.index('choice')
def test_dingtalk_card_markdown_preserves_internal_line_breaks():
assert _dingtalk_card_markdown('11\nQuestion\nCompleted') == '11<br>Question<br>Completed'
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()