Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/copaw/app/channels/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ def build_agent_request_from_user_content(

if not content_parts:
content_parts = [
TextContent(type=ContentType.TEXT, text=""),
TextContent(type=ContentType.TEXT, text=" "),
]
msg = Message(
type=MessageType.MESSAGE,
Expand Down
14 changes: 8 additions & 6 deletions src/copaw/app/channels/dingtalk/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,14 @@ def _parse_rich_content(
# Text may be under "text" or "content" (API variation).
item_text = item.get("text") or item.get("content")
if item_text is not None:
content.append(
TextContent(
type=ContentType.TEXT,
text=(item_text or "").strip(),
),
)
stripped = (item_text or "").strip()
if stripped:
content.append(
TextContent(
type=ContentType.TEXT,
text=stripped,
),
)
# Picture items may use pictureDownloadCode or downloadCode.
dl_code = (
item.get("downloadCode")
Expand Down
1 change: 1 addition & 0 deletions tests/unit/channels/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
74 changes: 74 additions & 0 deletions tests/unit/channels/test_dingtalk_rich_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
"""Tests for DingTalk rich text parsing - empty text blocks must be skipped."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

from copaw.app.channels.dingtalk.handler import DingTalkChannelHandler


def _make_handler() -> DingTalkChannelHandler:
"""Create a handler with stubbed dependencies."""
import asyncio

loop = asyncio.new_event_loop()
handler = DingTalkChannelHandler(
main_loop=loop,
enqueue_callback=None,
bot_prefix="",
download_url_fetcher=MagicMock(),
)
return handler


def _make_incoming(rich_text_items: list) -> MagicMock:
"""Build a mock incoming_message with richText content."""
msg = MagicMock()
msg.robot_code = "test_robot"
msg.to_dict.return_value = {"content": {"richText": rich_text_items}}
return msg


class TestParseRichContent:
"""Verify _parse_rich_content filters empty text blocks."""

def test_empty_text_items_are_skipped(self) -> None:
handler = _make_handler()
items = [
{"text": ""},
{"text": " "},
{"content": ""},
{"content": None},
]
result = handler._parse_rich_content(_make_incoming(items))
text_parts = [p for p in result if hasattr(p, "text")]
assert (
len(text_parts) == 0
), f"Expected no TextContent blocks, got {text_parts}"

def test_valid_text_items_are_kept(self) -> None:
handler = _make_handler()
items = [
{"text": "hello"},
{"text": " world "},
]
result = handler._parse_rich_content(_make_incoming(items))
text_parts = [p for p in result if hasattr(p, "text")]
assert len(text_parts) == 2
assert text_parts[0].text == "hello"
assert text_parts[1].text == "world"

def test_mixed_empty_and_valid(self) -> None:
handler = _make_handler()
items = [
{"text": ""},
{"text": "keep this"},
{"text": " "},
{"content": "also keep"},
]
result = handler._parse_rich_content(_make_incoming(items))
text_parts = [p for p in result if hasattr(p, "text")]
assert len(text_parts) == 2
assert text_parts[0].text == "keep this"
assert text_parts[1].text == "also keep"
Loading