From b484f1f3cd505e24310dad2dc490f2cb6ba2cb26 Mon Sep 17 00:00:00 2001 From: Molecule AI Infra-Runtime-BE Date: Sun, 10 May 2026 16:14:39 +0000 Subject: [PATCH] fix(workspace): push-mode Queued returns delivery_mode="push" (not silent default "poll") MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: a2a_response.py:197 returned Queued(method=method) without passing delivery_mode, silently defaulting to "poll" for push-mode busy-queue responses. Callers branching on v.delivery_mode would mis-identify push-mode responses as poll-mode, causing wrong dispatch logic. Fix: pass delivery_mode="push" explicitly in the push-mode branch. Tests: add push_queued_full/notify/no_method fixtures and 4 test cases asserting delivery_mode="push" for all three envelope shapes. Also add adversarial {"queued": "yes"} and {"queued": False} → Malformed guards. Co-Authored-By: Claude Opus 4.7 --- workspace/a2a_response.py | 2 +- workspace/tests/test_a2a_response.py | 57 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/workspace/a2a_response.py b/workspace/a2a_response.py index 769715fe..1741fef3 100644 --- a/workspace/a2a_response.py +++ b/workspace/a2a_response.py @@ -194,7 +194,7 @@ def parse(data: Any) -> Variant: method, data.get("queue_id", "?"), ) - return Queued(method=method) + return Queued(method=method, delivery_mode="push") # Poll-queued envelope. Both keys must be present — the workspace # server sets them together; if only one is present the body is diff --git a/workspace/tests/test_a2a_response.py b/workspace/tests/test_a2a_response.py index cf254b36..0311b9e2 100644 --- a/workspace/tests/test_a2a_response.py +++ b/workspace/tests/test_a2a_response.py @@ -105,6 +105,22 @@ _FIXTURES = { "status": "queued", "delivery_mode": "poll", }, + # Push-mode queue envelope: returned when a push-mode workspace is at + # capacity. The platform queues the request and returns + # {queued: true, message: "...", queue_id: "..."}. The ``delivery_mode`` + # field is not present in this envelope (distinguishes it from poll-mode). + "push_queued_full": { + "queued": True, + "method": "message/send", + "queue_id": "q-abc-123", + }, + "push_queued_notify": { + "queued": True, + "method": "notify", + }, + "push_queued_no_method": { + "queued": True, + }, "malformed_empty_dict": {}, "malformed_unexpected_keys": {"foo": "bar", "baz": 42}, "malformed_status_queued_no_delivery_mode": { @@ -159,6 +175,44 @@ class TestQueuedVariant: a2a_response.parse(_FIXTURES["poll_queued_full"]) assert any("queued for poll-mode peer" in r.message for r in caplog.records) + # --- Push-mode queue (handleA2ADispatchError → EnqueueA2A → 202 {queued: true}) --- + + def test_push_queued_full_returns_queued_with_delivery_mode_push(self): + # The push-mode path must set delivery_mode="push", not silently default to "poll". + # Callers that branch on v.delivery_mode will mis-route poll-mode responses + # as push-mode (and vice versa) if this field is wrong. + v = a2a_response.parse(_FIXTURES["push_queued_full"]) + assert isinstance(v, a2a_response.Queued) + assert v.method == "message/send" + assert v.delivery_mode == "push" + + def test_push_queued_notify(self): + v = a2a_response.parse(_FIXTURES["push_queued_notify"]) + assert isinstance(v, a2a_response.Queued) + assert v.method == "notify" + assert v.delivery_mode == "push" + + def test_push_queued_missing_method_defaults_to_message_send(self): + # Push-mode servers should always send method, but we handle absence gracefully. + v = a2a_response.parse(_FIXTURES["push_queued_no_method"]) + assert isinstance(v, a2a_response.Queued) + assert v.method == "message/send" + assert v.delivery_mode == "push" + + def test_push_queued_logs_queue_id(self, caplog): + with caplog.at_level(logging.INFO, logger="a2a_response"): + a2a_response.parse(_FIXTURES["push_queued_full"]) + assert any("q-abc-123" in r.message for r in caplog.records) + + def test_queued_string_yes_is_malformed_not_push_queued(self): + # ``{"queued": "yes"}`` is not True, so it must NOT enter the push branch. + v = a2a_response.parse({"queued": "yes"}) + assert isinstance(v, a2a_response.Malformed) + + def test_queued_false_is_malformed(self): + v = a2a_response.parse({"queued": False}) + assert isinstance(v, a2a_response.Malformed) + class TestResultVariant: """``parse()`` extracts the JSON-RPC ``result`` envelope into @@ -436,6 +490,9 @@ class TestRegressionGate: "poll_queued_full": a2a_response.Queued, "poll_queued_notify": a2a_response.Queued, "poll_queued_no_method": a2a_response.Queued, + "push_queued_full": a2a_response.Queued, + "push_queued_notify": a2a_response.Queued, + "push_queued_no_method": a2a_response.Queued, "malformed_empty_dict": a2a_response.Malformed, "malformed_unexpected_keys": a2a_response.Malformed, "malformed_status_queued_no_delivery_mode": a2a_response.Malformed,