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.
This commit is contained in:
fdc310
2026-07-11 17:28:31 +08:00
parent 56b3c15213
commit 183fb87649
4 changed files with 100 additions and 18 deletions
+1 -1
View File
@@ -602,9 +602,9 @@ def build_editor_data():
'conditionGroup': '',
'children': [
avatar('node_avatar', name='LangBot'),
markdown_block('node_text_content', variable='content'),
input_block('node_input'),
select_block('node_select'),
markdown_block('node_text_content', variable='content'),
button_group('node_btn_group'),
],
}
+53 -15
View File
@@ -213,12 +213,45 @@ def _dingtalk_pending_input_defs(form_data: dict) -> list[dict]:
def _dingtalk_clean_form_content(form_data: dict) -> str:
content = form_data.get('raw_form_content') or form_data.get('form_content') or ''
field_names = {
str(field.get('output_variable_name') or '').strip()
for field in _dingtalk_form_input_defs(form_data)
if str(field.get('output_variable_name') or '').strip()
}
is_field_step = bool(form_data.get('_current_input_field')) and not form_data.get('_action_select_only')
raw_content = str(form_data.get('raw_form_content') or '')
content = raw_content or form_data.get('form_content') or ''
input_defs = _dingtalk_form_input_defs(form_data)
field_names = {_dingtalk_field_name(field) for field in input_defs if _dingtalk_field_name(field)}
if is_field_step and raw_content:
current_field = str(form_data.get('_current_input_field') or '').strip()
current_placeholder = next(
(
match
for match in re.finditer(r'\{\{#\$output\.([^#{}]+)#\}\}', raw_content)
if match.group(1).strip() == current_field
),
None,
)
content = (
raw_content[: current_placeholder.end()] if current_placeholder else form_data.get('form_content') or ''
)
if form_data.get('_action_select_only') or is_field_step:
fields = {_dingtalk_field_name(field): field for field in input_defs if _dingtalk_field_name(field)}
inputs = form_data.get('inputs') or {}
def replace_placeholder(match: re.Match[str]) -> str:
field_name = match.group(1).strip()
field = fields.get(field_name)
if not field or inputs.get(field_name) in (None, '', []):
return ''
lines = _dingtalk_completed_input_lines(
{
'input_defs': [field],
'inputs': {field_name: inputs[field_name]},
}
)
return lines[0] if lines else ''
content = re.sub(r'\{\{#\$output\.([^#{}]+)#\}\}', replace_placeholder, str(content))
kept_lines: list[str] = []
for line in str(content).splitlines():
placeholder = re.fullmatch(r'\s*\{\{#\$output\.([^#{}]+)#\}\}\s*', line)
@@ -228,6 +261,11 @@ def _dingtalk_clean_form_content(form_data: dict) -> str:
return re.sub(r'\n{3,}', '\n\n', '\n'.join(kept_lines).strip())
def _dingtalk_card_markdown(content: str) -> str:
"""Preserve line breaks inside DingTalk card-template markdown slots."""
return '<br>'.join(str(content or '').splitlines())
def _dingtalk_form_input_defs(form_data: dict) -> list[dict]:
return list(form_data.get('all_input_defs') or form_data.get('input_defs') or [])
@@ -261,11 +299,11 @@ def _dingtalk_completed_input_lines(form_data: dict) -> list[str]:
field_type = _dingtalk_field_type(field)
display_value = _dingtalk_display_input_value(field, value)
if field_type == 'select':
lines.append(f'✅ 已选择 {field_name}**{display_value}**')
lines.append(f'✅ 已选择 {field_name}{display_value}')
elif field_type in {'file', 'file-list'}:
lines.append(f'✅ 已上传 {field_name}**{display_value}**')
lines.append(f'✅ 已上传 {field_name}{display_value}')
else:
lines.append(f'✅ 已填写 {field_name}**{display_value}**')
lines.append(f'✅ 已填写 {field_name}{display_value}')
return lines
@@ -915,9 +953,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if node_title:
parts.append(f'**{node_title}**')
if form_content:
parts.append(form_content)
parts.append(_dingtalk_card_markdown(form_content))
completed_lines = _dingtalk_completed_input_lines(form_data)
if completed_lines:
if completed_lines and not all(line in form_content for line in completed_lines):
parts.append('<hr>' + '<br>'.join(completed_lines))
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
if input_hint_lines:
@@ -1017,9 +1055,9 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if node_title:
parts.append(f'**{node_title}**')
if form_content:
parts.append(form_content)
parts.append(_dingtalk_card_markdown(form_content))
completed_lines = _dingtalk_completed_input_lines(form_data)
if completed_lines:
if completed_lines and not all(line in form_content for line in completed_lines):
parts.append('<hr>' + '<br>'.join(completed_lines))
input_hint_lines = [] if native_field else _dingtalk_input_hint_lines(form_data)
if input_hint_lines:
@@ -1457,14 +1495,14 @@ class DingTalkAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
if node_title:
parts.append(f'**{node_title}**')
if form_content:
parts.append(form_content)
parts.append(_dingtalk_card_markdown(form_content))
completed_lines = _dingtalk_completed_input_lines(
{
'input_defs': input_defs or [],
'inputs': inputs or {},
}
)
if completed_lines:
if completed_lines and not all(line in form_content for line in completed_lines):
parts.append('<hr>' + '<br>'.join(completed_lines))
parts.append(f'<hr>✅ 已选择:**{action_title}**')
content = '<br><br>'.join(parts)
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ 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,
@@ -92,7 +93,7 @@ def test_dingtalk_completed_input_lines_include_text_and_select_values():
}
)
assert lines == ['✅ 已填写 comment**looks good**', '✅ 已选择 choice**B**']
assert lines == ['✅ 已填写 commentlooks good', '✅ 已选择 choiceB']
def test_dingtalk_clean_form_content_uses_all_input_defs():
@@ -110,6 +111,49 @@ def test_dingtalk_clean_form_content_uses_all_input_defs():
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={