diff --git a/tests/e2e/test_poll_mode_e2e.sh b/tests/e2e/test_poll_mode_e2e.sh index e4dd22bc..766ec3c7 100755 --- a/tests/e2e/test_poll_mode_e2e.sh +++ b/tests/e2e/test_poll_mode_e2e.sh @@ -157,6 +157,43 @@ A2A_RESP=$(curl -s --max-time "$TIMEOUT" -X POST "$BASE/workspaces/$POLL_WS_ID/a }') check "poll-mode A2A returns queued status" '"status":"queued"' "$A2A_RESP" + +# ---------- Phase 3.5: Python parser classifies queued envelope correctly ---------- +# (#2967) — server emits the queued envelope, the wheel's a2a_response.parse() +# MUST classify it as the Queued variant, not Malformed. Pre-#2967 the bare +# message/send parser in a2a_client.py:587 misclassified this and returned +# "[A2A_ERROR] unexpected response shape", which broke external↔external A2A +# on poll-mode peers. +# +# This phase exercises the actual on-the-wire response from a real +# workspace-server (NOT a mocked dict) through the same module the production +# wheel ships, so a regression in either the server emit shape OR the client +# parser fails this E2E. + +echo "" +echo "--- Phase 3.5: Python parser classifies real server response (#2967) ---" + +# Pipe the queued response captured above through a2a_response.parse and +# assert the classification. WORKSPACE_ID is required at module import +# time but irrelevant to this parsing call (any UUID is fine). +PARSE_RESULT=$(WORKSPACE_ID="00000000-0000-0000-0000-000000000001" \ + python3 -c " +import json, sys +sys.path.insert(0, '$(cd "$(dirname "$0")/../../workspace" && pwd)') +import a2a_response +data = json.loads(r'''$A2A_RESP''') +v = a2a_response.parse(data) +print(type(v).__name__) +if isinstance(v, a2a_response.Queued): + print(f'method={v.method} delivery_mode={v.delivery_mode}') +") + +check_eq "Python parser classifies real server response as Queued" \ + "Queued" "$(printf '%s' "$PARSE_RESULT" | head -n1)" +check "Queued variant captures method=message/send" \ + "method=message/send" "$PARSE_RESULT" +check "Queued variant captures delivery_mode=poll" \ + "delivery_mode=poll" "$PARSE_RESULT" check "queued response echoes delivery_mode=poll" '"delivery_mode":"poll"' "$A2A_RESP" check "queued response echoes the JSON-RPC method" '"method":"message/send"' "$A2A_RESP" diff --git a/workspace/tests/test_a2a_client.py b/workspace/tests/test_a2a_client.py index 97a8c739..39e3ae04 100644 --- a/workspace/tests/test_a2a_client.py +++ b/workspace/tests/test_a2a_client.py @@ -281,11 +281,11 @@ class TestSendA2AMessage: to the 'unexpected response shape' error path → callers retried, peer got duplicate delegations. - Pin: poll-queued envelope returns a clean success string that does - NOT start with _A2A_ERROR_PREFIX, so callers route it through the - normal-outcome path. Verified discriminating: assert_NOT_startswith - the error prefix would FAIL on the old code (which returned an - error-prefixed string) and PASSES on the new code. + Pin: poll-queued envelope returns a string tagged with the + _A2A_QUEUED_PREFIX sentinel (not _A2A_ERROR_PREFIX), so callers + can branch on the typed outcome without substring-sniffing. + Verified discriminating: pre-fix returned _A2A_ERROR_PREFIX so + the not-startswith assertion would FAIL on the old code. """ import a2a_client @@ -301,12 +301,13 @@ class TestSendA2AMessage: # Discriminating: pre-fix returned a string that startswith # _A2A_ERROR_PREFIX, so this assertion would have FAILED on the - # old code. New code returns a queued-success string. + # old code. New code returns the queued-success sentinel. assert not result.startswith(a2a_client._A2A_ERROR_PREFIX), ( f"poll-queued envelope must not be tagged as A2A error; got: {result!r}" ) - assert "queued" in result.lower() - assert "poll" in result.lower() + assert result.startswith(a2a_client._A2A_QUEUED_PREFIX), ( + f"poll-queued envelope must use the queued sentinel; got: {result!r}" + ) # The method is included so a structured-log scraper can route by # protocol verb if needed. assert "message/send" in result @@ -329,6 +330,7 @@ class TestSendA2AMessage: result = await a2a_client.send_a2a_message(_TEST_PEER_ID, "task") assert not result.startswith(a2a_client._A2A_ERROR_PREFIX) + assert result.startswith(a2a_client._A2A_QUEUED_PREFIX) assert "message/sendStream" in result async def test_status_queued_without_poll_mode_still_falls_through(self):