Skip to content

Maeste/issue37#47

Merged
maeste merged 27 commits into
a2aproject:mainfrom
maeste:maeste/issue37
Aug 18, 2025
Merged

Maeste/issue37#47
maeste merged 27 commits into
a2aproject:mainfrom
maeste:maeste/issue37

Conversation

@maeste

@maeste maeste commented Aug 5, 2025

Copy link
Copy Markdown
Collaborator

Description

Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make sure it goes smoothly:

  • Follow the CONTRIBUTING Guide.
  • Make your Pull Request title in the https://www.conventionalcommits.org/ specification.
    • Important Prefixes for release-please:
      • fix: which represents bug fixes, and correlates to a SemVer patch.
      • feat: represents a new feature, and correlates to a SemVer minor.
      • feat!:, or fix!:, refactor!:, etc., which represent a breaking change (indicated by the !) and will result in a SemVer major.
  • Ensure the tests pass
  • Appropriate READMEs were updated (if necessary)

Fixes #37

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

Gemini encountered an error creating the summary. You can try again by commenting /gemini summary.

@maeste

maeste commented Aug 5, 2025

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a significant and well-structured refactoring to support multi-transport testing for A2A v0.3.0. The new transport layer is a solid architectural choice. However, there are several critical issues, including misleading placeholder implementations for the gRPC and JSON-RPC streaming clients, and non-functional tests that need to be addressed. Once these are fixed, this will be an excellent addition to the project.

Comment on lines +1 to +41
"""
gRPC transport client for A2A Protocol v0.3.0

Implements real gRPC communication with A2A SUTs using Protocol Buffers.
This client makes actual network calls to live SUTs - NO MOCKING.

Specification Reference: A2A Protocol v0.3.0 §4.2 - gRPC Transport
"""

import json
import logging
from typing import Dict, List, Optional, Any, AsyncIterator, Union
from urllib.parse import urlparse
import asyncio

import grpc
from google.protobuf.struct_pb2 import Struct
from google.protobuf.timestamp_pb2 import Timestamp

from tck.transport.base_client import BaseTransportClient, TransportType, TransportError

logger = logging.getLogger(__name__)


class GRPCClient(BaseTransportClient):
"""
A2A gRPC transport client for real network communication.

This client implements the A2A Protocol v0.3.0 gRPC transport specification,
making actual gRPC calls to live SUTs. All methods perform real network
operations without any mocking.

Key Features:
- Real gRPC communication using Protocol Buffers
- Async/await support for streaming operations
- Automatic message format conversion (JSON ↔ Protobuf)
- Full A2A v0.3.0 method coverage
- Transport-specific error handling

Specification Reference: A2A Protocol v0.3.0 §4.2
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The docstring and comments in this file are highly misleading. They claim this is a real gRPC client that makes network calls, but the implementation is entirely placeholder code with simulated responses. This gives a false sense of completeness and is a significant risk.

The file should either be fully implemented or be clearly marked as a stub/placeholder in its name, docstrings, and comments until it is functional.

Comment on lines +384 to +598
def test_transport_specific_method_naming(self, sut_client: BaseTransportClient):
"""
Test that transport-specific method naming follows A2A v0.3.0 conventions.

A2A v0.3.0 Specification Reference: §3.5.1, §3.5.2, §3.5.3

Validates:
- JSON-RPC: {category}/{action} pattern
- gRPC: PascalCase compound words
- REST: /v1/{resource}[/{id}][:{action}] pattern
"""
transport_type = get_client_transport_type(sut_client)

if transport_type == "jsonrpc":
# Test JSON-RPC category/action naming pattern
test_methods = [
"message/send",
"tasks/get",
"tasks/cancel",
"agent/getAuthenticatedExtendedCard"
]

for method in test_methods:
# Verify method follows category/action pattern
assert "/" in method, f"JSON-RPC method {method} should use category/action pattern"
parts = method.split("/")
assert len(parts) >= 2, f"Method {method} should have at least category/action"

# Category should be lowercase noun
category = parts[0]
assert category.islower(), f"Category {category} should be lowercase"

elif transport_type == "grpc":
# Test gRPC PascalCase naming
if hasattr(sut_client, '_get_method_mapping'):
method_mapping = sut_client._get_method_mapping()
for grpc_method in method_mapping.values():
# Should be PascalCase
assert grpc_method[0].isupper(), f"gRPC method {grpc_method} should start with uppercase"
assert grpc_method.replace("_", "").isalnum(), f"gRPC method {grpc_method} should be alphanumeric"

elif transport_type == "rest":
# Test REST URL pattern compliance
if hasattr(sut_client, '_get_url_patterns'):
url_patterns = sut_client._get_url_patterns()
for pattern in url_patterns.values():
# Should start with /v1/
assert pattern.startswith("/v1/"), f"REST URL {pattern} should start with /v1/"

# Should follow resource-based pattern
if ":send" in pattern:
assert "/message:send" in pattern
elif ":cancel" in pattern:
assert "/tasks/" in pattern and ":cancel" in pattern


class TestTransportSpecificFeatures:
"""
Test suite for transport-specific features and optimizations (§3.4.3)

Validates that transports provide transport-specific extensions while
maintaining functional equivalence with core A2A functionality.
"""

@pytest.mark.optional
@pytest.mark.a2a_v030
def test_grpc_specific_features(self, sut_client: BaseTransportClient):
"""
Test gRPC-specific features like bidirectional streaming and metadata.

A2A v0.3.0 Specification Reference: §3.4.3

Validates:
- Bidirectional streaming support (if implemented)
- gRPC metadata handling
- Protocol Buffers serialization
"""
if get_client_transport_type(sut_client) != "grpc":
pytest.skip("Test only applicable to gRPC transport")

# Test gRPC metadata support
if hasattr(sut_client, 'send_message_with_metadata'):
try:
metadata = {"custom-header": "test-value"}
task = sut_client.send_message_with_metadata(
"Test message with metadata",
metadata=metadata
)
assert task is not None

except Exception as e:
if "not implemented" in str(e).lower():
pytest.skip("gRPC metadata not implemented")
else:
pytest.fail(f"gRPC metadata test failed: {e}")

# Test bidirectional streaming (if supported)
if hasattr(sut_client, 'bidirectional_stream'):
try:
# Test basic bidirectional streaming capability
stream = sut_client.bidirectional_stream()
assert stream is not None

except Exception as e:
if "not implemented" in str(e).lower():
pytest.skip("gRPC bidirectional streaming not implemented")
else:
pytest.fail(f"gRPC bidirectional streaming test failed: {e}")

@pytest.mark.optional
@pytest.mark.a2a_v030
def test_rest_specific_features(self, sut_client: BaseTransportClient):
"""
Test REST-specific features like HTTP caching and conditional requests.

A2A v0.3.0 Specification Reference: §3.4.3

Validates:
- HTTP caching headers
- Conditional request support (If-Modified-Since, ETag)
- Proper HTTP status codes
"""
if get_client_transport_type(sut_client) != "rest":
pytest.skip("Test only applicable to REST transport")

# Test HTTP caching headers
if hasattr(sut_client, 'get_with_caching'):
try:
response = sut_client.get_with_caching("/v1/card")

# Should include caching headers
headers = getattr(response, 'headers', {})
cache_headers = ['cache-control', 'etag', 'last-modified']

has_cache_header = any(header in headers for header in cache_headers)
if not has_cache_header:
# Not required, but note if missing
pass

except Exception as e:
if "not implemented" in str(e).lower():
pytest.skip("REST caching features not implemented")
else:
pytest.fail(f"REST caching test failed: {e}")

# Test conditional requests
if hasattr(sut_client, 'conditional_get'):
try:
# First request to get ETag/Last-Modified
response1 = sut_client.get_agent_card()

# Second request with conditional headers
response2 = sut_client.conditional_get("/v1/card")

# Should handle conditional requests appropriately
assert response2 is not None

except Exception as e:
if "not implemented" in str(e).lower():
pytest.skip("REST conditional requests not implemented")
else:
pytest.fail(f"REST conditional request test failed: {e}")

@pytest.mark.optional
@pytest.mark.a2a_v030
def test_jsonrpc_specific_features(self, sut_client: BaseTransportClient):
"""
Test JSON-RPC-specific features and extensions.

A2A v0.3.0 Specification Reference: §3.4.3

Validates:
- Additional fields in JSON-RPC objects (non-conflicting)
- Batch request support (if implemented)
- JSON-RPC 2.0 compliance
"""
if get_client_transport_type(sut_client) != "jsonrpc":
pytest.skip("Test only applicable to JSON-RPC transport")

# Test batch requests (if supported)
if hasattr(sut_client, 'batch_request'):
try:
batch_requests = [
{"method": "agent/getCard", "params": {}, "id": 1},
{"method": "message/send", "params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "test"}],
"messageId": "test-batch-1"
}
}, "id": 2}
]

responses = sut_client.batch_request(batch_requests)
assert isinstance(responses, list)
assert len(responses) == len(batch_requests)

except Exception as e:
if "not implemented" in str(e).lower():
pytest.skip("JSON-RPC batch requests not implemented")
else:
pytest.fail(f"JSON-RPC batch request test failed: {e}")

# Test additional fields in responses (should not break spec compliance)
try:
task = transport_send_message(sut_client, "Test for additional fields")

# Task may have additional fields beyond spec
spec_fields = {"id", "contextId", "status", "history", "artifacts", "metadata", "kind"}
task_fields = set(task.keys())

# Additional fields are allowed as long as spec fields are present
missing_required = {"id", "status", "kind"} - task_fields
assert not missing_required, f"Missing required fields: {missing_required}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The test classes TestMethodMappingCompliance and TestTransportSpecificFeatures contain multiple tests that rely on helper methods that do not exist on the client implementations (e.g., _get_method_mapping, send_message_with_metadata, batch_request).

These tests are currently non-functional and will fail with AttributeError. They should either be implemented correctly along with the required client methods or be marked with pytest.skip until they are ready. Committing non-functional tests pollutes the test suite.

Comment thread tests/mandatory/protocol/test_a2a_v030_new_methods.py Outdated
Comment on lines +187 to +205
def send_streaming_message(self, message: Dict[str, Any],
extra_headers: Optional[Dict[str, str]] = None) -> Iterator[Dict[str, Any]]:
"""
Send a message with streaming response using message/stream method.

Makes a real JSON-RPC call with Server-Sent Events to test streaming functionality.

Args:
message: The message object conforming to A2A Message schema
extra_headers: Optional HTTP headers

Returns:
Iterator that yields task updates from the real SUT stream

Raises:
JSONRPCError: If streaming message sending fails

Specification Reference: A2A Protocol v0.3.0 §3.3 - Streaming Transport
"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The docstring for send_streaming_message states that it "Makes a real JSON-RPC call with Server-Sent Events", but the implementation does not handle SSE. It makes a single request and yields the initial response, which is not streaming.

This is misleading and fails to test the actual streaming capability of the SUT. The implementation must be corrected to handle an SSE stream, or the docstring should be updated to reflect that it's a placeholder.

Comment thread tests/mandatory/protocol/test_a2a_v030_new_methods.py Outdated
Comment thread tck/agent_card_utils.py
if not transport_type:
errors.append(f"Unknown transport type in additionalInterfaces: {transport_name}")

return errors No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file is missing a final newline character. According to PEP 8, all source files should end with a single newline. This applies to several other new files in this pull request as well.

Suggested change
return errors
return errors

Comment thread tck/agent_card_utils.py
Comment on lines +399 to +403
transport_name = interface.get("transport") or interface.get("type")
if transport_name:
transport_type = _parse_transport_type(transport_name)
if not transport_type:
errors.append(f"Unknown transport type in additionalInterfaces: {transport_name}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment on line 394 mentions checking for "orphaned endpoints", but the implementation only checks for unknown transport types. An orphaned endpoint (one with a URL but no transport type) would not be caught by the current logic.

You should add a check for interfaces that have an endpoint or url but are missing a transport or type.

Suggested change
transport_name = interface.get("transport") or interface.get("type")
if transport_name:
transport_type = _parse_transport_type(transport_name)
if not transport_type:
errors.append(f"Unknown transport type in additionalInterfaces: {transport_name}")
transport_name = interface.get("transport") or interface.get("type")
endpoint = interface.get("endpoint") or interface.get("url")
if endpoint and not transport_name:
errors.append(f"Orphaned endpoint found with no transport type: {endpoint}")
elif transport_name:
transport_type = _parse_transport_type(transport_name)
if not transport_type:
errors.append(f"Unknown transport type in additionalInterfaces: {transport_name}")

Comment thread README.md
Comment on lines +400 to +408

**A2A v0.3.0 Functional Equivalence Requirements** (per specification §3.4.1):
```json
{
"additionalInterfaces": [
{"url": "...", "transport": "JSONRPC"}, ← Must test equivalence
{"url": "...", "transport": "GRPC"}, ← if multiple declared
{"url": "...", "transport": "HTTP+JSON"}
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The code block is marked as json, but it contains comments (← ...) which are not valid in standard JSON. This can be confusing for readers and may cause issues for tools that try to parse it.

Consider using jsonc (JSON with Comments) for the language identifier or moving the comments outside the code block to ensure correctness.

Suggested change
**A2A v0.3.0 Functional Equivalence Requirements** (per specification §3.4.1):
```json
{
"additionalInterfaces": [
{"url": "...", "transport": "JSONRPC"}, ← Must test equivalence
{"url": "...", "transport": "GRPC"}, ← if multiple declared
{"url": "...", "transport": "HTTP+JSON"}
]
{
"additionalInterfaces": [
{"url": "...", "transport": "JSONRPC"}, // Must test equivalence
{"url": "...", "transport": "GRPC"}, // if multiple declared
{"url": "...", "transport": "HTTP+JSON"}
]
}

Comment thread run_tck.py Outdated
return result.returncode

def run_all_categories(sut_url: str, verbose: bool = False, verbose_log: bool = False, generate_report: bool = False, compliance_report: str = None):
def run_all_categories(sut_url: str, verbose: bool = False, verbose_log: bool = False, generate_report: bool = False, compliance_report: str = None, transport_strategy: str = None, preferred_transport: str = None, disabled_transports: str = None, enable_equivalence_testing: bool = None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function signature is excessively long, which harms readability and maintainability. It's good practice to break long signatures across multiple lines, with one argument per line.

def run_all_categories(
    sut_url: str,
    verbose: bool = False,
    verbose_log: bool = False,
    generate_report: bool = False,
    compliance_report: str = None,
    transport_strategy: str = None,
    preferred_transport: str = None,
    disabled_transports: str = None,
    enable_equivalence_testing: bool = None,
):

Comment thread run_tck.py Outdated
Comment thread README.md
result_error_code = error.get("code")

assert result_error_code == base_error_code, \
f"Error code mismatch for non-existent task: {base_transport}={base_error_code} vs {transport_type}={result_error_code}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running this, I'm seeing:

E AssertionError: Error code mismatch for non-existent task: grpc=-32603 vs jsonrpc=-32001

How is the error code for gRPC being translated to -32603? For gRPC, the status code in this case would be expected to be grpc.StatusCode.NOT_FOUND.

# Both should indicate parameter validation errors
# (-32602 for invalid params, or implementation-specific codes)
assert result_error_code == base_error_code, \
f"Error code mismatch for invalid params: {base_transport}={base_error_code} vs {transport_type}={result_error_code}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar question here. The expected gRPC status code would be grpc.StatusCode.INVALID_ARGUMENT.

@maeste maeste marked this pull request as ready for review August 18, 2025 20:38
@maeste maeste merged commit 67a86c0 into a2aproject:main Aug 18, 2025
@maeste maeste deleted the maeste/issue37 branch August 18, 2025 20:38
unclaimed-cherry-blossom pushed a commit to unclaimed-cherry-blossom/a2a-tck that referenced this pull request Feb 11, 2026
# Description

Thank you for opening a Pull Request!
Before submitting your PR, there are a few things you can do to make
sure it goes smoothly:

- [x] Follow the [`CONTRIBUTING` Guide](../CONTRIBUTING.md).
- [x] Make your Pull Request title in the
<https://www.conventionalcommits.org/> specification.
- Important Prefixes for
[release-please](https://github.com/googleapis/release-please):
- `fix:` which represents bug fixes, and correlates to a
[SemVer](https://semver.org/) patch.
- `feat:` represents a new feature, and correlates to a SemVer minor.
- `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking
change (indicated by the `!`) and will result in a SemVer major.
- [x] Ensure the tests pass
- [x] Appropriate READMEs were updated (if necessary)

Fixes a2aproject#37

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Porting to spec 0.3.0

3 participants