-
Notifications
You must be signed in to change notification settings - Fork 395
test: add infra-independent unit tests for core agent protocols #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| """Unit tests for A2A protocol endpoint.""" | ||
|
|
||
| import json | ||
| import uuid | ||
| from unittest.mock import AsyncMock, MagicMock, patch | ||
|
|
||
| import pytest | ||
| from starlette.requests import Request | ||
| from starlette.responses import Response | ||
|
|
||
| from bindu.common.protocol.types import ( | ||
| InternalError, | ||
| JSONParseError, | ||
| MethodNotFoundError, | ||
| ) | ||
| from bindu.server.applications import BinduApplication | ||
| from bindu.server.endpoints.a2a_protocol import agent_run_endpoint | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_app(): | ||
| """Create a mock BinduApplication.""" | ||
| app = MagicMock(spec=BinduApplication) | ||
| app.task_manager = MagicMock() | ||
| # Mock the handler method on task_manager | ||
| app.task_manager.mock_handler = AsyncMock(return_value={"result": "success"}) | ||
| return app | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_settings(): | ||
| """Mock app settings.""" | ||
| with patch("bindu.server.endpoints.a2a_protocol.app_settings") as mock: | ||
| mock.agent.method_handlers = { | ||
| "tasks/list": "mock_handler", | ||
| "message/send": "mock_handler", | ||
| } | ||
| mock.auth.enabled = False | ||
| mock.auth.require_permissions = False | ||
| yield mock | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_valid_request(mock_app, mock_settings): | ||
| """Test successful processing of a valid A2A request.""" | ||
| # Setup request with valid JSON body | ||
| body = { | ||
| "jsonrpc": "2.0", | ||
| "method": "tasks/list", | ||
| "params": {}, | ||
| "id": str(uuid.uuid4()), | ||
| } | ||
|
|
||
| mock_request = MagicMock(spec=Request) | ||
| mock_request.body = AsyncMock(return_value=json.dumps(body).encode()) | ||
|
|
||
| class MockState: | ||
| pass | ||
| mock_request.state = MockState() | ||
|
|
||
| # Call endpoint | ||
| response = await agent_run_endpoint(mock_app, mock_request) | ||
|
|
||
| # Verify task manager handler was called | ||
| mock_app.task_manager.mock_handler.assert_called_once() | ||
|
|
||
| # Verify response | ||
| assert isinstance(response, Response) | ||
| assert response.status_code == 200 | ||
| content = json.loads(response.body) | ||
| assert content["result"] == "success" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_invalid_json(mock_app): | ||
| """Test handling of invalid JSON body.""" | ||
| mock_request = MagicMock(spec=Request) | ||
| mock_request.body = AsyncMock(return_value=b"invalid json") | ||
|
|
||
| response = await agent_run_endpoint(mock_app, mock_request) | ||
|
|
||
| assert response.status_code == 400 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_unsupported_method(mock_app, mock_settings): | ||
| """Test handling of unsupported method.""" | ||
| body = { | ||
| "jsonrpc": "2.0", | ||
| "method": "unknown/method", | ||
| "id": str(uuid.uuid4()), | ||
| } | ||
|
|
||
| mock_request = MagicMock(spec=Request) | ||
| mock_request.body = AsyncMock(return_value=json.dumps(body).encode()) | ||
|
|
||
| response = await agent_run_endpoint(mock_app, mock_request) | ||
|
|
||
| assert response.status_code == 400 | ||
| # Validation fails for unknown method tag in discriminated union | ||
|
|
||
|
Comment on lines
+85
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find the Pydantic schema definition for A2A requests to verify valid method tags
rg -nP -A5 'class.*A2ARequest|a2a_request_ta.*=' --type=pyRepository: GetBindu/Bindu Length of output: 580 🏁 Script executed: #!/bin/bash
set -euo pipefail
# 1) Find where A2ARequest is defined and how the discriminator works
rg -n "class A2ARequest|A2ARequest\s*=" bindu/common/protocol/types.py
rg -n "discriminator|Field\\(.*discriminator|tag\\b|method\\b.*discriminator" bindu/common/protocol/types.py
# 2) Show the relevant region around A2ARequest
python3 - <<'PY'
import re, pathlib
p = pathlib.Path("bindu/common/protocol/types.py")
txt = p.read_text(encoding="utf-8")
m = re.search(r"class\s+A2ARequest\b", txt)
if not m:
# fallback: look for type alias
m = re.search(r"A2ARequest\s*=", txt)
if not m:
print("Could not find A2ARequest definition marker")
raise SystemExit(1)
start = max(0, txt.count("\n", 0, m.start()) - 20)
# compute line number for m.start()
line = txt[:m.start()].count("\n") + 1
print(f"A2ARequest definition around line ~{line}")
PY
# Print a bounded chunk around the definition (first hit by line number)
LINE=$(python3 - <<'PY'
import re, pathlib
p = pathlib.Path("bindu/common/protocol/types.py")
txt = p.read_text(encoding="utf-8")
for pat in [r"class\s+A2ARequest\b", r"A2ARequest\s*="]:
m = re.search(pat, txt)
if m:
line = txt[:m.start()].count("\n")+1
print(line)
break
else:
raise SystemExit(1)
PY
)
sed -n "$((LINE-40)),$((LINE+120))p" bindu/common/protocol/types.py
# 3) Inspect endpoint error paths in a2a_protocol.py
rg -n "agent_run_endpoint|status_code\s*==|return .*400|return .*404|MethodNotFoundError|JSONParseError|validation" tests -S
rg -n "agent_run_endpoint" -S
fd -e py a2a_protocol.py bindu tests || true
# Use fd to locate if different path exists
fd -e py "a2a_protocol.py" . || true
# Then display surrounding code where 400/404 are set
FOUND=$(fd -e py "a2a_protocol.py" . | head -n 1)
if [ -n "$FOUND" ]; then
rg -n "MethodNotFoundError|JSONParseError|status_code|return" "$FOUND"
# Show the chunk around where the handler-not-found logic likely is
rg -n "MethodNotFoundError" "$FOUND" | head -n 1 | awk -F: '{print $1}' | while read -r L; do
sed -n "$((L-60)),$((L+80))p" "$FOUND"
done
fiRepository: GetBindu/Bindu Length of output: 52212 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Show the test code around the target lines
sed -n '70,130p' tests/unit/server/endpoints/test_a2a_protocol.py
# Locate and show endpoint error handling blocks (avoid negative ranges)
rg -n "MethodNotFoundError|JSONParseError|status_code\s*=\s*400|status_code\s*=\s*404" bindu/server/endpoints/a2a_protocol.py
rg -n "agent_run_endpoint" bindu/server/endpoints/a2a_protocol.py
# Show a chunk around the endpoint's main logic
# (use line numbers from rg to pick a non-negative window)
ENDPOINT_LINE=$(rg -n "async def agent_run_endpoint" bindu/server/endpoints/a2a_protocol.py | head -n 1 | cut -d: -f1)
START=$((ENDPOINT_LINE-80)); if [ "$START" -lt 1 ]; then START=1; fi
sed -n "${START},$((ENDPOINT_LINE+220))p" bindu/server/endpoints/a2a_protocol.pyRepository: GetBindu/Bindu Length of output: 9984 Clarify error path:
🤖 Prompt for AI Agents |
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_internal_error(mock_app, mock_settings): | ||
| """Test handling of internal errors.""" | ||
| body = { | ||
| "jsonrpc": "2.0", | ||
| "method": "tasks/list", | ||
| "params": {}, | ||
| "id": str(uuid.uuid4()), | ||
| } | ||
|
|
||
| mock_request = MagicMock(spec=Request) | ||
| mock_request.body = AsyncMock(return_value=json.dumps(body).encode()) | ||
|
|
||
| class MockState: | ||
| pass | ||
| mock_request.state = MockState() | ||
|
|
||
| # Simulate handler raising exception | ||
| mock_app.task_manager.mock_handler.side_effect = Exception("Crash") | ||
|
|
||
| response = await agent_run_endpoint(mock_app, mock_request) | ||
|
|
||
| assert response.status_code == 500 | ||
| content = json.loads(response.body) | ||
| assert "error" in content | ||
| assert content["error"]["code"] == -32603 # Internal error | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_payment_context_injection(mock_app): | ||
| """Test injection of payment context into message metadata.""" | ||
| # Mock settings specifically for this test to ensure handler is found | ||
| with patch("bindu.server.endpoints.a2a_protocol.app_settings") as mock_settings: | ||
| mock_settings.agent.method_handlers = {"message/send": "mock_handler"} | ||
| mock_settings.auth.enabled = False | ||
| mock_settings.auth.require_permissions = False | ||
|
|
||
| body = { | ||
| "jsonrpc": "2.0", | ||
| "method": "message/send", | ||
| "params": { | ||
| "configuration": { | ||
| "acceptedOutputModes": ["text"] | ||
| }, | ||
| "message": { | ||
| "messageId": "123e4567-e89b-12d3-a456-426614174000", | ||
| "contextId": "123e4567-e89b-12d3-a456-426614174001", | ||
| "taskId": "123e4567-e89b-12d3-a456-426614174002", | ||
| "kind": "message", | ||
| "role": "user", | ||
| "parts": [{"kind": "text", "text": "hello"}] | ||
| } | ||
| }, | ||
| "id": str(uuid.uuid4()), | ||
| } | ||
|
|
||
| mock_request = MagicMock(spec=Request) | ||
| mock_request.body = AsyncMock(return_value=json.dumps(body).encode()) | ||
|
|
||
| # Setup payment context in request state | ||
| class MockState: | ||
| payment_payload = {"amount": 100} | ||
| payment_requirements = {"token": "USDC"} | ||
| verify_response = {"status": "verified"} | ||
| mock_request.state = MockState() | ||
|
|
||
| # Call endpoint | ||
| await agent_run_endpoint(mock_app, mock_request) | ||
|
|
||
| # Verify handler called with modified request | ||
| call_args = mock_app.task_manager.mock_handler.call_args[0][0] | ||
| metadata = call_args["params"]["message"]["metadata"] | ||
|
|
||
| assert "_payment_context" in metadata | ||
| context = metadata["_payment_context"] | ||
| assert context["payment_payload"] == {"amount": 100} | ||
| assert context["payment_requirements"] == {"token": "USDC"} | ||
| assert context["verify_response"] == {"status": "verified"} | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mock handler should return complete JSON-RPC 2.0 response structure.
The handler currently returns
{"result": "success"}, but a valid JSON-RPC 2.0 response must include"jsonrpc": "2.0"and"id"fields. Real handlers return complete responses, and this test should validate the full protocol contract.📋 Proposed fix to return complete JSON-RPC response
Then update the test assertion at line 71 to validate all required fields:
📝 Committable suggestion
🤖 Prompt for AI Agents