Skip to content

Commit 41ea0b3

Browse files
committed
test: add tests for adk_thought metadata round-trip in part_converter
Add tests verifying that convert_a2a_part_to_genai_part() correctly restores thought=True from A2A TextPart metadata. Without this round-trip preservation, the thought-filtering logic in _handle_a2a_response has no effect on completed artifact parts, causing internal reasoning to leak into user-facing output. Also adds an end-to-end test reproducing the exact scenario from #4676: mixed thought/non-thought parts serialized as A2A artifacts must survive round-trip conversion so downstream filtering works correctly. Refs #4676
1 parent d9ec43f commit 41ea0b3

1 file changed

Lines changed: 94 additions & 0 deletions

File tree

tests/unittests/a2a/converters/test_part_converter.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,70 @@ def test_convert_text_part(self):
4949
assert isinstance(result, genai_types.Part)
5050
assert result.text == "Hello, world!"
5151

52+
def test_convert_text_part_with_thought_metadata(self):
53+
"""Test conversion of A2A TextPart with adk_thought metadata to GenAI Part.
54+
55+
Verifies that the inbound conversion restores thought=True from A2A
56+
metadata, which is essential for the thought-filtering logic in
57+
RemoteA2aAgent._handle_a2a_response. See #4676.
58+
"""
59+
# Arrange
60+
a2a_part = a2a_types.Part(
61+
root=a2a_types.TextPart(
62+
text="internal reasoning",
63+
metadata={_get_adk_metadata_key("thought"): True},
64+
)
65+
)
66+
67+
# Act
68+
result = convert_a2a_part_to_genai_part(a2a_part)
69+
70+
# Assert
71+
assert result is not None
72+
assert isinstance(result, genai_types.Part)
73+
assert result.text == "internal reasoning"
74+
assert result.thought is True
75+
76+
def test_convert_text_part_with_thought_false_metadata(self):
77+
"""Test conversion of A2A TextPart with adk_thought=False metadata."""
78+
# Arrange
79+
a2a_part = a2a_types.Part(
80+
root=a2a_types.TextPart(
81+
text="user-facing text",
82+
metadata={_get_adk_metadata_key("thought"): False},
83+
)
84+
)
85+
86+
# Act
87+
result = convert_a2a_part_to_genai_part(a2a_part)
88+
89+
# Assert
90+
assert result is not None
91+
assert result.text == "user-facing text"
92+
# thought=False means it's not a thought part; the filter won't match
93+
assert result.thought is False
94+
95+
def test_convert_text_part_without_thought_metadata(self):
96+
"""Test conversion of A2A TextPart without adk_thought metadata.
97+
98+
When no thought metadata is present, thought should remain None.
99+
"""
100+
# Arrange
101+
a2a_part = a2a_types.Part(
102+
root=a2a_types.TextPart(
103+
text="regular text",
104+
metadata={"some_other_key": "value"},
105+
)
106+
)
107+
108+
# Act
109+
result = convert_a2a_part_to_genai_part(a2a_part)
110+
111+
# Assert
112+
assert result is not None
113+
assert result.text == "regular text"
114+
assert result.thought is None
115+
52116
def test_convert_file_part_with_uri(self):
53117
"""Test conversion of A2A FilePart with URI to GenAI Part."""
54118
# Arrange
@@ -562,6 +626,36 @@ def test_text_part_with_thought_round_trip(self):
562626
assert result_genai_part.text == original_text
563627
assert result_genai_part.thought
564628

629+
def test_thought_round_trip_enables_filtering(self):
630+
"""Test that thought round-trip enables downstream filtering.
631+
632+
This reproduces the exact scenario from #4676: an A2A response
633+
contains both thought and non-thought parts serialized as artifacts.
634+
After round-tripping through part_converter, the thought flag must
635+
be preserved so that filtering (e.g., in RemoteA2aAgent) can remove
636+
thought parts from user-facing output.
637+
"""
638+
# Simulate outbound: GenAI parts -> A2A parts (server side)
639+
thought_genai = genai_types.Part(
640+
text="<internal reasoning text>", thought=True
641+
)
642+
answer_genai = genai_types.Part(text="<final user-facing answer>")
643+
644+
thought_a2a = convert_genai_part_to_a2a_part(thought_genai)
645+
answer_a2a = convert_genai_part_to_a2a_part(answer_genai)
646+
647+
# Simulate inbound: A2A parts -> GenAI parts (client side)
648+
restored_thought = convert_a2a_part_to_genai_part(thought_a2a)
649+
restored_answer = convert_a2a_part_to_genai_part(answer_a2a)
650+
651+
# Apply the filter that RemoteA2aAgent uses
652+
parts = [restored_thought, restored_answer]
653+
filtered = [p for p in parts if not p.thought]
654+
655+
# Only the user-facing answer should survive
656+
assert len(filtered) == 1
657+
assert filtered[0].text == "<final user-facing answer>"
658+
565659
def test_file_uri_round_trip(self):
566660
"""Test round-trip conversion for file parts with URI."""
567661
# Arrange

0 commit comments

Comments
 (0)