diff --git a/tests/test_protocols.py b/tests/test_protocols.py index e7c016ec..a06bad17 100644 --- a/tests/test_protocols.py +++ b/tests/test_protocols.py @@ -930,6 +930,98 @@ async def test_task_id_cleared_on_unknown_state(self, a2a_config): assert adapter.active_task_id is None + @pytest.mark.asyncio + async def test_rejected_task_result_content(self, a2a_config): + """TASK_STATE_REJECTED — adapter returns SUBMITTED status with 'rejected' + in metadata and the TextPart message. REJECTED is terminal (task_id is + cleared) but routes through the non-COMPLETED else-branch in + _process_task_response, so data=None.""" + adapter = A2AAdapter(a2a_config) + + rejected = create_mock_a2a_task( + task_id="task-rejected-content", + context_id="ctx-rej", + state="rejected", + parts=[TextPart(text="policy violation: brand safety")], + ) + mock_a2a_client = AsyncMock() + mock_a2a_client.send_message = AsyncMock( + return_value=SendMessageSuccessResponse(result=rejected) + ) + + with patch.object(adapter, "_get_a2a_client", return_value=mock_a2a_client): + result = await adapter._call_a2a_tool("create_media_buy", {}) + + assert result.status == TaskStatus.SUBMITTED + assert result.data is None + assert result.message == "policy violation: brand safety" + assert result.metadata is not None + assert result.metadata["status"] == "rejected" + + @pytest.mark.asyncio + async def test_rejected_task_adcp_error_datapart_not_extracted(self, a2a_config): + """TASK_STATE_REJECTED with a DataPart carrying adcp_error — the + DataPart is silently dropped because _process_task_response only + calls _extract_result_from_task for COMPLETED tasks. This test + documents the current gap: callers cannot read structured error + detail from a rejected task's artifact without a separate fix.""" + adapter = A2AAdapter(a2a_config) + + rejected = create_mock_a2a_task( + task_id="task-rejected-err", + context_id="ctx-rej-err", + state="rejected", + parts=[ + DataPart(data={"adcp_error": {"code": "POLICY_VIOLATION", "message": "rejected"}}), + TextPart(text="rejected by server"), + ], + ) + mock_a2a_client = AsyncMock() + mock_a2a_client.send_message = AsyncMock( + return_value=SendMessageSuccessResponse(result=rejected) + ) + + with patch.object(adapter, "_get_a2a_client", return_value=mock_a2a_client): + result = await adapter._call_a2a_tool("create_media_buy", {}) + + assert result.status == TaskStatus.SUBMITTED + # Gap: adcp_error DataPart is not extracted for non-COMPLETED states. + # A future fix should surface structured error detail here. + assert result.data is None + assert result.message == "rejected by server" + assert result.metadata is not None + assert result.metadata["status"] == "rejected" + + @pytest.mark.asyncio + async def test_auth_required_task_result_content(self, a2a_config): + """TASK_STATE_AUTH_REQUIRED — non-terminal state. Adapter returns + SUBMITTED status with 'auth-required' in metadata and the challenge + message from the TextPart. Callers should surface this to trigger + an auth flow before re-submitting.""" + adapter = A2AAdapter(a2a_config) + + auth_task = create_mock_a2a_task( + task_id="task-auth-content", + context_id="ctx-auth", + state="auth-required", + parts=[TextPart(text="OAuth required: redirect to https://auth.example.com")], + ) + mock_a2a_client = AsyncMock() + mock_a2a_client.send_message = AsyncMock( + return_value=SendMessageSuccessResponse(result=auth_task) + ) + + with patch.object(adapter, "_get_a2a_client", return_value=mock_a2a_client): + result = await adapter._call_a2a_tool("create_media_buy", {}) + + assert result.status == TaskStatus.SUBMITTED + assert result.data is None + assert result.message == "OAuth required: redirect to https://auth.example.com" + assert result.metadata is not None + assert result.metadata["status"] == "auth-required" + assert result.metadata["task_id"] == "task-auth-content" + assert result.metadata["context_id"] == "ctx-auth" + @pytest.mark.asyncio async def test_state_not_committed_when_post_processing_raises(self, a2a_config): """If _process_task_response raises, the adapter must NOT advance