"""Regression tests for ToolMessage content normalization in serialization. Ensures that structured content (list-of-blocks) is properly extracted to plain text, preventing raw Python repr strings from reaching the UI. See: https://github.com/bytedance/deer-flow/issues/1149 """ from langchain_core.messages import ToolMessage from deerflow.client import DeerFlowClient # --------------------------------------------------------------------------- # _serialize_message # --------------------------------------------------------------------------- class TestSerializeToolMessageContent: """DeerFlowClient._serialize_message should normalize ToolMessage content.""" def test_string_content(self): msg = ToolMessage(content="ok", tool_call_id="tc1", name="search") result = DeerFlowClient._serialize_message(msg) assert result["content"] == "ok" assert result["type"] == "tool" def test_list_of_blocks_content(self): """List-of-blocks should be extracted, not repr'd.""" msg = ToolMessage( content=[{"type": "text", "text": "hello world"}], tool_call_id="tc1", name="search", ) result = DeerFlowClient._serialize_message(msg) assert result["content"] == "hello world" # Must NOT contain Python repr artifacts assert "[" not in result["content"] assert "{" not in result["content"] def test_multiple_text_blocks(self): """Multiple full text blocks should be joined with newlines.""" msg = ToolMessage( content=[ {"type": "text", "text": "line 1"}, {"type": "text", "text": "line 2"}, ], tool_call_id="tc1", name="search", ) result = DeerFlowClient._serialize_message(msg) assert result["content"] == "line 1\nline 2" def test_string_chunks_are_joined_without_newlines(self): """Chunked string payloads should not get artificial separators.""" msg = ToolMessage( content=['{"a"', ': "b"}'], tool_call_id="tc1", name="search", ) result = DeerFlowClient._serialize_message(msg) assert result["content"] == '{"a": "b"}' def test_mixed_string_chunks_and_blocks(self): """String chunks stay contiguous, but text blocks remain separated.""" msg = ToolMessage( content=["prefix", "-continued", {"type": "text", "text": "block text"}], tool_call_id="tc1", name="search", ) result = DeerFlowClient._serialize_message(msg) assert result["content"] == "prefix-continued\nblock text" def test_mixed_blocks_with_non_text(self): """Non-text blocks (e.g. image) should be skipped gracefully.""" msg = ToolMessage( content=[ {"type": "text", "text": "found results"}, {"type": "image_url", "image_url": {"url": "http://img.png"}}, ], tool_call_id="tc1", name="view_image", ) result = DeerFlowClient._serialize_message(msg) assert result["content"] == "found results" def test_empty_list_content(self): msg = ToolMessage(content=[], tool_call_id="tc1", name="search") result = DeerFlowClient._serialize_message(msg) assert result["content"] == "" def test_plain_string_in_list(self): """Bare strings inside a list should be kept.""" msg = ToolMessage( content=["plain text block"], tool_call_id="tc1", name="search", ) result = DeerFlowClient._serialize_message(msg) assert result["content"] == "plain text block" def test_unknown_content_type_falls_back(self): """Unexpected types should not crash — return str().""" msg = ToolMessage(content=42, tool_call_id="tc1", name="calc") result = DeerFlowClient._serialize_message(msg) # int → not str, not list → falls to str() assert result["content"] == "42" # --------------------------------------------------------------------------- # _extract_text (already existed, but verify it also covers ToolMessage paths) # --------------------------------------------------------------------------- class TestExtractText: """DeerFlowClient._extract_text should handle all content shapes.""" def test_string_passthrough(self): assert DeerFlowClient._extract_text("hello") == "hello" def test_list_text_blocks(self): assert DeerFlowClient._extract_text([{"type": "text", "text": "hi"}]) == "hi" def test_empty_list(self): assert DeerFlowClient._extract_text([]) == "" def test_fallback_non_iterable(self): assert DeerFlowClient._extract_text(123) == "123"