Skip to content

Commit 7be949c

Browse files
bokelleyclaude
andauthored
test(a2a): assert TaskResult content for rejected and auth-required states (#270)
* test(a2a): assert TaskResult content for rejected and auth-required states Closes #263 (item 3 — test coverage gaps for A2A 1.0 states). Prior tests for TASK_STATE_REJECTED and TASK_STATE_AUTH_REQUIRED only asserted adapter.active_task_id behavior. These new tests also capture the full TaskResult: status, data, message, and metadata["status"]. The gap-documenting test (adcp_error DataPart not extracted for non-COMPLETED states) explicitly asserts data=None today and flags the future fix path in a comment. https://claude.ai/code/session_01MxALGppjocjuZzDVp48tps * test(a2a): address review — strict xfail + parity assertions Acting on code-reviewer feedback on this PR: - Convert the gap-documenting test from a passing assertion of buggy behavior to ``pytest.mark.xfail(strict=True)`` asserting the desired post-fix behavior. Strict mode flips the test to failure when someone fixes the gap, so we keep the regression-trip benefit without the test name reading as endorsement of the bug. - Split the original mixed-payload test into two: one strict-xfail for the structured-error extraction we want, one passing test that pins the TextPart-extraction half (regression guard for the human message surfacing across REJECTED with a DataPart present). - Add ``task_id``/``context_id`` assertions to the REJECTED test for parity with the AUTH_REQUIRED test — the else-branch in ``_process_task_response`` always sets both, regardless of state. - Add ``result.error is None`` and ``result.success is True`` assertions to both REJECTED and AUTH_REQUIRED tests. The ``success=True`` line is annotated as ``# FIXME: semantically wrong for REJECTED`` so future-us has a hook for the eventual fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2cfa1c6 commit 7be949c

1 file changed

Lines changed: 142 additions & 0 deletions

File tree

tests/test_protocols.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,148 @@ async def test_task_id_cleared_on_unknown_state(self, a2a_config):
930930

931931
assert adapter.active_task_id is None
932932

933+
@pytest.mark.asyncio
934+
async def test_rejected_task_result_content(self, a2a_config):
935+
"""TASK_STATE_REJECTED — adapter routes through the non-COMPLETED
936+
else-branch in _process_task_response (a2a.py:618-673), returning
937+
``status=SUBMITTED``, ``data=None``, ``success=True`` (the branch
938+
hardcodes success — see follow-up tracker), and metadata carrying
939+
the lowercased state, the task id, and the context id."""
940+
adapter = A2AAdapter(a2a_config)
941+
942+
rejected = create_mock_a2a_task(
943+
task_id="task-rejected-content",
944+
context_id="ctx-rej",
945+
state="rejected",
946+
parts=[TextPart(text="policy violation: brand safety")],
947+
)
948+
mock_a2a_client = AsyncMock()
949+
mock_a2a_client.send_message = AsyncMock(
950+
return_value=SendMessageSuccessResponse(result=rejected)
951+
)
952+
953+
with patch.object(adapter, "_get_a2a_client", return_value=mock_a2a_client):
954+
result = await adapter._call_a2a_tool("create_media_buy", {})
955+
956+
assert result.status == TaskStatus.SUBMITTED
957+
assert result.data is None
958+
assert result.error is None
959+
assert result.success is True # FIXME: semantically wrong for REJECTED
960+
assert result.message == "policy violation: brand safety"
961+
assert result.metadata is not None
962+
assert result.metadata["status"] == "rejected"
963+
assert result.metadata["task_id"] == "task-rejected-content"
964+
assert result.metadata["context_id"] == "ctx-rej"
965+
966+
@pytest.mark.xfail(
967+
strict=True,
968+
reason=(
969+
"_process_task_response only extracts adcp_error DataParts for COMPLETED "
970+
"tasks; rejected tasks lose structured error detail. When this gap is "
971+
"fixed (issue #263), flip the test to assert the extracted error "
972+
"instead of removing xfail."
973+
),
974+
)
975+
@pytest.mark.asyncio
976+
async def test_rejected_task_adcp_error_datapart_extracted(self, a2a_config):
977+
"""TASK_STATE_REJECTED with a DataPart carrying adcp_error.
978+
979+
Strict-xfail: today the DataPart's structured error detail is silently
980+
dropped because the adapter only calls ``_extract_result_from_task``
981+
for COMPLETED tasks. This test asserts the desired post-fix behavior
982+
(error code accessible to callers); ``strict=True`` flips to failure
983+
the moment someone fixes the gap so we don't keep documenting it as
984+
endorsed."""
985+
adapter = A2AAdapter(a2a_config)
986+
987+
rejected = create_mock_a2a_task(
988+
task_id="task-rejected-err",
989+
context_id="ctx-rej-err",
990+
state="rejected",
991+
parts=[
992+
DataPart(data={"adcp_error": {"code": "POLICY_VIOLATION", "message": "rejected"}}),
993+
TextPart(text="rejected by server"),
994+
],
995+
)
996+
mock_a2a_client = AsyncMock()
997+
mock_a2a_client.send_message = AsyncMock(
998+
return_value=SendMessageSuccessResponse(result=rejected)
999+
)
1000+
1001+
with patch.object(adapter, "_get_a2a_client", return_value=mock_a2a_client):
1002+
result = await adapter._call_a2a_tool("create_media_buy", {})
1003+
1004+
# Desired post-fix: structured error surfaces on TaskResult.
1005+
assert result.error is not None
1006+
assert "POLICY_VIOLATION" in (result.error or "")
1007+
1008+
@pytest.mark.asyncio
1009+
async def test_rejected_task_drops_datapart_keeps_textpart(self, a2a_config):
1010+
"""TASK_STATE_REJECTED with both a DataPart and a TextPart — pin the
1011+
current behavior: the TextPart message is preserved, the DataPart's
1012+
structured payload is not extracted (tracked separately in the strict
1013+
xfail above). This guards against a regression that drops the human
1014+
message too."""
1015+
adapter = A2AAdapter(a2a_config)
1016+
1017+
rejected = create_mock_a2a_task(
1018+
task_id="task-rejected-mixed",
1019+
context_id="ctx-rej-mixed",
1020+
state="rejected",
1021+
parts=[
1022+
DataPart(data={"adcp_error": {"code": "POLICY_VIOLATION", "message": "rejected"}}),
1023+
TextPart(text="rejected by server"),
1024+
],
1025+
)
1026+
mock_a2a_client = AsyncMock()
1027+
mock_a2a_client.send_message = AsyncMock(
1028+
return_value=SendMessageSuccessResponse(result=rejected)
1029+
)
1030+
1031+
with patch.object(adapter, "_get_a2a_client", return_value=mock_a2a_client):
1032+
result = await adapter._call_a2a_tool("create_media_buy", {})
1033+
1034+
assert result.status == TaskStatus.SUBMITTED
1035+
assert result.data is None
1036+
assert result.message == "rejected by server"
1037+
assert result.metadata is not None
1038+
assert result.metadata["status"] == "rejected"
1039+
assert result.metadata["task_id"] == "task-rejected-mixed"
1040+
assert result.metadata["context_id"] == "ctx-rej-mixed"
1041+
1042+
@pytest.mark.asyncio
1043+
async def test_auth_required_task_result_content(self, a2a_config):
1044+
"""TASK_STATE_AUTH_REQUIRED — non-terminal state. Same else-branch
1045+
as REJECTED in _process_task_response, so the assertions parallel
1046+
``test_rejected_task_result_content``: status SUBMITTED, data None,
1047+
metadata with state, task id, and context id, plus the challenge
1048+
message extracted from the TextPart."""
1049+
adapter = A2AAdapter(a2a_config)
1050+
1051+
auth_task = create_mock_a2a_task(
1052+
task_id="task-auth-content",
1053+
context_id="ctx-auth",
1054+
state="auth-required",
1055+
parts=[TextPart(text="OAuth required: redirect to https://auth.example.com")],
1056+
)
1057+
mock_a2a_client = AsyncMock()
1058+
mock_a2a_client.send_message = AsyncMock(
1059+
return_value=SendMessageSuccessResponse(result=auth_task)
1060+
)
1061+
1062+
with patch.object(adapter, "_get_a2a_client", return_value=mock_a2a_client):
1063+
result = await adapter._call_a2a_tool("create_media_buy", {})
1064+
1065+
assert result.status == TaskStatus.SUBMITTED
1066+
assert result.data is None
1067+
assert result.error is None
1068+
assert result.success is True
1069+
assert result.message == "OAuth required: redirect to https://auth.example.com"
1070+
assert result.metadata is not None
1071+
assert result.metadata["status"] == "auth-required"
1072+
assert result.metadata["task_id"] == "task-auth-content"
1073+
assert result.metadata["context_id"] == "ctx-auth"
1074+
9331075
@pytest.mark.asyncio
9341076
async def test_state_not_committed_when_post_processing_raises(self, a2a_config):
9351077
"""If _process_task_response raises, the adapter must NOT advance

0 commit comments

Comments
 (0)