From 185c0efd23bfc6227f9edc2fb6e7447c061f8245 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 17 Apr 2026 15:32:34 +0000 Subject: [PATCH 1/3] fix: update with_a2a_extensions to append instead of overwriting --- src/a2a/client/service_parameters.py | 22 +++++-- tests/client/test_service_parameters.py | 78 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 tests/client/test_service_parameters.py diff --git a/src/a2a/client/service_parameters.py b/src/a2a/client/service_parameters.py index cef250807..f880082ea 100644 --- a/src/a2a/client/service_parameters.py +++ b/src/a2a/client/service_parameters.py @@ -1,7 +1,10 @@ from collections.abc import Callable from typing import TypeAlias -from a2a.extensions.common import HTTP_EXTENSION_HEADER +from a2a.extensions.common import ( + HTTP_EXTENSION_HEADER, + get_requested_extensions, +) ServiceParameters: TypeAlias = dict[str, str] @@ -46,15 +49,26 @@ def create_from( def with_a2a_extensions(extensions: list[str]) -> ServiceParametersUpdate: """Create a ServiceParametersUpdate that adds A2A extensions. + Merges the supplied URIs with any extensions already present in the + A2A-Extensions service parameter, deduplicating and producing a stable + (sorted) order. Calling this multiple times in a chain accumulates the + requested extensions instead of overwriting prior values. + Args: - extensions: List of extension strings. + extensions: List of extension URIs to advertise. Returns: A function that updates ServiceParameters with the extensions header. """ def update(parameters: ServiceParameters) -> None: - if extensions: - parameters[HTTP_EXTENSION_HEADER] = ','.join(extensions) + if not extensions: + return + existing = parameters.get(HTTP_EXTENSION_HEADER) + merged = sorted( + get_requested_extensions([existing] if existing else []) + | set(extensions) + ) + parameters[HTTP_EXTENSION_HEADER] = ','.join(merged) return update diff --git a/tests/client/test_service_parameters.py b/tests/client/test_service_parameters.py new file mode 100644 index 000000000..1c628be64 --- /dev/null +++ b/tests/client/test_service_parameters.py @@ -0,0 +1,78 @@ +"""Tests for a2a.client.service_parameters module.""" + +from a2a.client.service_parameters import ( + ServiceParametersFactory, + with_a2a_extensions, +) +from a2a.extensions.common import HTTP_EXTENSION_HEADER + + +def test_with_a2a_extensions_sets_header_when_empty(): + """First call on empty parameters sets the joined URIs.""" + parameters = ServiceParametersFactory.create( + [with_a2a_extensions(['ext-b', 'ext-a'])] + ) + + assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b' + + +def test_with_a2a_extensions_merges_disjoint_calls(): + """A second call with disjoint URIs unions both sets.""" + parameters = ServiceParametersFactory.create( + [ + with_a2a_extensions(['ext-a']), + with_a2a_extensions(['ext-b']), + ] + ) + + assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b' + + +def test_with_a2a_extensions_deduplicates_overlapping(): + """Overlapping URIs do not produce duplicates.""" + parameters = ServiceParametersFactory.create( + [ + with_a2a_extensions(['ext-a', 'ext-b']), + with_a2a_extensions(['ext-b', 'ext-c']), + ] + ) + + assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b,ext-c' + + +def test_with_a2a_extensions_empty_is_noop(): + """Calling with an empty list leaves any existing header untouched.""" + parameters = ServiceParametersFactory.create( + [ + with_a2a_extensions(['ext-a']), + with_a2a_extensions([]), + ] + ) + + assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a' + + +def test_with_a2a_extensions_empty_does_not_create_header(): + """Calling with an empty list on empty parameters adds nothing.""" + parameters = ServiceParametersFactory.create([with_a2a_extensions([])]) + + assert HTTP_EXTENSION_HEADER not in parameters + + +def test_with_a2a_extensions_output_is_sorted(): + """Output ordering is deterministic (sorted) regardless of input order.""" + parameters = ServiceParametersFactory.create( + [with_a2a_extensions(['c', 'a', 'b'])] + ) + + assert parameters[HTTP_EXTENSION_HEADER] == 'a,b,c' + + +def test_with_a2a_extensions_merges_existing_header_value(): + """Existing comma-separated header values are parsed and merged.""" + base = ServiceParametersFactory.create_from( + {HTTP_EXTENSION_HEADER: 'ext-a, ext-b'}, + [with_a2a_extensions(['ext-c'])], + ) + + assert base[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b,ext-c' From 97bc6fe5a5dc7a378bc8308745bbc2c9178ab447 Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 17 Apr 2026 15:41:45 +0000 Subject: [PATCH 2/3] Update --- src/a2a/client/service_parameters.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/a2a/client/service_parameters.py b/src/a2a/client/service_parameters.py index f880082ea..39fe79ce1 100644 --- a/src/a2a/client/service_parameters.py +++ b/src/a2a/client/service_parameters.py @@ -47,28 +47,18 @@ def create_from( def with_a2a_extensions(extensions: list[str]) -> ServiceParametersUpdate: - """Create a ServiceParametersUpdate that adds A2A extensions. + """Create a ServiceParametersUpdate that merges A2A extension URIs. - Merges the supplied URIs with any extensions already present in the - A2A-Extensions service parameter, deduplicating and producing a stable - (sorted) order. Calling this multiple times in a chain accumulates the - requested extensions instead of overwriting prior values. - - Args: - extensions: List of extension URIs to advertise. - - Returns: - A function that updates ServiceParameters with the extensions header. + Unions the supplied URIs with any already present in the A2A-Extensions + parameter, deduplicating and emitting them in sorted order. Repeated + calls accumulate rather than overwrite. """ def update(parameters: ServiceParameters) -> None: if not extensions: return - existing = parameters.get(HTTP_EXTENSION_HEADER) - merged = sorted( - get_requested_extensions([existing] if existing else []) - | set(extensions) - ) + existing = parameters.get(HTTP_EXTENSION_HEADER, '') + merged = sorted(get_requested_extensions([existing, *extensions])) parameters[HTTP_EXTENSION_HEADER] = ','.join(merged) return update From d9126acea0ec99072edf91f6247bf2ed1f467ebb Mon Sep 17 00:00:00 2001 From: Ivan Shymko Date: Fri, 17 Apr 2026 15:42:53 +0000 Subject: [PATCH 3/3] Update --- tests/client/test_service_parameters.py | 61 ++++++++----------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/tests/client/test_service_parameters.py b/tests/client/test_service_parameters.py index 1c628be64..fbabd9719 100644 --- a/tests/client/test_service_parameters.py +++ b/tests/client/test_service_parameters.py @@ -7,41 +7,30 @@ from a2a.extensions.common import HTTP_EXTENSION_HEADER -def test_with_a2a_extensions_sets_header_when_empty(): - """First call on empty parameters sets the joined URIs.""" - parameters = ServiceParametersFactory.create( - [with_a2a_extensions(['ext-b', 'ext-a'])] - ) - - assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b' - - -def test_with_a2a_extensions_merges_disjoint_calls(): - """A second call with disjoint URIs unions both sets.""" +def test_with_a2a_extensions_merges_dedupes_and_sorts(): + """Repeated calls accumulate; duplicates collapse; output is sorted.""" parameters = ServiceParametersFactory.create( [ - with_a2a_extensions(['ext-a']), - with_a2a_extensions(['ext-b']), + with_a2a_extensions(['ext-c', 'ext-a']), + with_a2a_extensions(['ext-b', 'ext-a']), ] ) - assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b' + assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b,ext-c' -def test_with_a2a_extensions_deduplicates_overlapping(): - """Overlapping URIs do not produce duplicates.""" - parameters = ServiceParametersFactory.create( - [ - with_a2a_extensions(['ext-a', 'ext-b']), - with_a2a_extensions(['ext-b', 'ext-c']), - ] +def test_with_a2a_extensions_merges_existing_header_value(): + """Pre-existing comma-separated header values are parsed and merged.""" + parameters = ServiceParametersFactory.create_from( + {HTTP_EXTENSION_HEADER: 'ext-a, ext-b'}, + [with_a2a_extensions(['ext-c'])], ) assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b,ext-c' def test_with_a2a_extensions_empty_is_noop(): - """Calling with an empty list leaves any existing header untouched.""" + """An empty extensions list leaves the header untouched / absent.""" parameters = ServiceParametersFactory.create( [ with_a2a_extensions(['ext-a']), @@ -50,29 +39,15 @@ def test_with_a2a_extensions_empty_is_noop(): ) assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a' - - -def test_with_a2a_extensions_empty_does_not_create_header(): - """Calling with an empty list on empty parameters adds nothing.""" - parameters = ServiceParametersFactory.create([with_a2a_extensions([])]) - - assert HTTP_EXTENSION_HEADER not in parameters - - -def test_with_a2a_extensions_output_is_sorted(): - """Output ordering is deterministic (sorted) regardless of input order.""" - parameters = ServiceParametersFactory.create( - [with_a2a_extensions(['c', 'a', 'b'])] + assert HTTP_EXTENSION_HEADER not in ServiceParametersFactory.create( + [with_a2a_extensions([])] ) - assert parameters[HTTP_EXTENSION_HEADER] == 'a,b,c' - -def test_with_a2a_extensions_merges_existing_header_value(): - """Existing comma-separated header values are parsed and merged.""" - base = ServiceParametersFactory.create_from( - {HTTP_EXTENSION_HEADER: 'ext-a, ext-b'}, - [with_a2a_extensions(['ext-c'])], +def test_with_a2a_extensions_normalizes_input_strings(): + """Input strings are split on commas and stripped, like header values.""" + parameters = ServiceParametersFactory.create( + [with_a2a_extensions(['ext-a, ext-b', ' ext-c '])] ) - assert base[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b,ext-c' + assert parameters[HTTP_EXTENSION_HEADER] == 'ext-a,ext-b,ext-c'