Skip to content

Commit 59df380

Browse files
Copilotg3force
andcommitted
Address code review feedback
- Create constants.py with shared EXTERNAL_TOKEN_SESSION_KEY constant - Create test_fixtures.py with shared test helper functions - Update agent.py and agent_to_a2a.py to use shared constant - Make comment about header provider neutral (not referencing specific header) - Remove useless test_external_token_stored_in_session test - Simplify tests by removing unnecessary async decorators - Move imports to top of test files instead of inside functions - Update test_a2a_starlette.py to use shared test fixtures - All 14 tests passing - All linters passing (ruff, mypy) Co-authored-by: g3force <779094+g3force@users.noreply.github.com>
1 parent b7859d0 commit 59df380

File tree

6 files changed

+63
-145
lines changed

6 files changed

+63
-145
lines changed

adk/agenticlayer/agent.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,39 @@
77
import httpx
88
from a2a.client import A2ACardResolver
99
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH
10-
from google.adk.agents.readonly_context import ReadonlyContext
1110
from google.adk.agents import BaseAgent, LlmAgent
1211
from google.adk.agents.llm_agent import ToolUnion
12+
from google.adk.agents.readonly_context import ReadonlyContext
1313
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
1414
from google.adk.tools.agent_tool import AgentTool
1515
from google.adk.tools.mcp_tool import StreamableHTTPConnectionParams
1616
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
1717
from httpx_retries import Retry, RetryTransport
1818

1919
from agenticlayer.config import InteractionType, McpTool, SubAgent
20+
from agenticlayer.constants import EXTERNAL_TOKEN_SESSION_KEY
2021

2122
logger = logging.getLogger(__name__)
2223

23-
# Key used to retrieve the external token from the ADK session state
24-
# This must match the key used in agent_to_a2a.py
25-
_EXTERNAL_TOKEN_SESSION_KEY = "__external_token__"
26-
2724

2825
def _get_mcp_headers_from_session(readonly_context: ReadonlyContext) -> dict[str, str]:
2926
"""Header provider function for MCP tools that retrieves token from ADK session.
3027
3128
This function is called by the ADK when MCP tools are invoked. It reads the
32-
X-External-Token from the session state where it was stored during request
29+
external token from the session state where it was stored during request
3330
processing by TokenCapturingA2aAgentExecutor.
3431
3532
Args:
3633
readonly_context: The ADK ReadonlyContext providing access to the session
3734
3835
Returns:
3936
A dictionary of headers to include in MCP tool requests.
40-
If a token is stored in the session, includes the X-External-Token header.
37+
If a token is stored in the session, includes it in the headers.
4138
"""
4239
# Access the session state through the readonly context
4340
# The session state is a dict that can contain the external token
4441
if readonly_context and readonly_context.session:
45-
external_token = readonly_context.session.state.get(_EXTERNAL_TOKEN_SESSION_KEY)
42+
external_token = readonly_context.session.state.get(EXTERNAL_TOKEN_SESSION_KEY)
4643
if external_token:
4744
return {"X-External-Token": external_token}
4845
return {}
@@ -132,7 +129,7 @@ def load_tools(self, mcp_tools: list[McpTool]) -> list[ToolUnion]:
132129
url=str(tool.url),
133130
timeout=tool.timeout,
134131
),
135-
# Pass header provider that retrieves X-External-Token from ADK session
132+
# Provide header provider to inject session-stored token into tool requests
136133
header_provider=_get_mcp_headers_from_session,
137134
)
138135
)

adk/agenticlayer/agent_to_a2a.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,10 @@
2929
from .agent import AgentFactory
3030
from .callback_tracer_plugin import CallbackTracerPlugin
3131
from .config import McpTool, SubAgent
32+
from .constants import EXTERNAL_TOKEN_SESSION_KEY
3233

3334
logger = logging.getLogger(__name__)
3435

35-
# Key used to store the external token in the ADK session state
36-
_EXTERNAL_TOKEN_SESSION_KEY = "__external_token__"
37-
3836

3937
class TokenCapturingA2aAgentExecutor(A2aAgentExecutor):
4038
"""Custom A2A agent executor that captures and stores the X-External-Token header.
@@ -76,7 +74,7 @@ async def _prepare_session(
7674
if external_token:
7775
# Store the token in the session state with a private key
7876
# The session state is mutable and changes are persisted automatically
79-
session.state[_EXTERNAL_TOKEN_SESSION_KEY] = external_token
77+
session.state[EXTERNAL_TOKEN_SESSION_KEY] = external_token
8078
logger.debug("Stored external token in session %s", session.id)
8179

8280
return session

adk/agenticlayer/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"""Constants shared across the agenticlayer package."""
2+
3+
# Key used to store the external token in the ADK session state
4+
EXTERNAL_TOKEN_SESSION_KEY = "__external_token__"

adk/tests/test_a2a_starlette.py

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import contextlib
2-
import uuid
32
from collections.abc import AsyncIterator
43
from typing import Any
54

@@ -13,12 +12,13 @@
1312
from agenticlayer.config import InteractionType, McpTool, SubAgent
1413
from asgi_lifespan import LifespanManager
1514
from google.adk.agents.llm_agent import LlmAgent
16-
from google.adk.models.lite_llm import LiteLlm
1715
from httpx import Response
1816
from httpx_retries import Retry
1917
from pydantic import AnyHttpUrl
2018
from starlette.testclient import TestClient
2119

20+
from .test_fixtures import create_agent, create_send_message_request
21+
2222

2323
def create_mock_agent_card(
2424
agent_name: str,
@@ -39,39 +39,6 @@ def create_mock_agent_card(
3939
}
4040

4141

42-
def create_send_message_request(
43-
message_text: str = "Hello, agent!",
44-
) -> dict[str, Any]:
45-
"""Helper function to create a valid A2A send message request."""
46-
message_id = str(uuid.uuid4())
47-
context_id = str(uuid.uuid4())
48-
return {
49-
"jsonrpc": "2.0",
50-
"id": 1,
51-
"method": "message/send",
52-
"params": {
53-
"message": {
54-
"role": "user",
55-
"parts": [{"kind": "text", "text": message_text}],
56-
"messageId": message_id,
57-
"contextId": context_id,
58-
},
59-
"metadata": {},
60-
},
61-
}
62-
63-
64-
def create_agent(
65-
name: str = "test_agent",
66-
) -> LlmAgent:
67-
return LlmAgent(
68-
name=name,
69-
model=LiteLlm(model="gemini/gemini-2.5-flash"),
70-
description="Test agent",
71-
instruction="You are a test agent.",
72-
)
73-
74-
7542
@pytest_asyncio.fixture
7643
def app_factory() -> Any:
7744
@contextlib.asynccontextmanager

adk/tests/test_external_token.py

Lines changed: 8 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,19 @@
11
"""Tests for external token passing to MCP tools via ADK session."""
22

3-
import uuid
4-
from typing import Any
5-
6-
import pytest
7-
from agenticlayer.agent import AgentFactory
8-
from agenticlayer.agent_to_a2a import _EXTERNAL_TOKEN_SESSION_KEY, to_a2a
9-
from agenticlayer.config import McpTool
10-
from asgi_lifespan import LifespanManager
11-
from google.adk.agents.llm_agent import LlmAgent
12-
from google.adk.models.lite_llm import LiteLlm
13-
from httpx_retries import Retry
14-
from pydantic import AnyHttpUrl
15-
from starlette.testclient import TestClient
16-
17-
18-
def create_send_message_request(
19-
message_text: str = "Hello, agent!",
20-
) -> dict[str, Any]:
21-
"""Helper function to create a valid A2A send message request."""
22-
message_id = str(uuid.uuid4())
23-
context_id = str(uuid.uuid4())
24-
return {
25-
"jsonrpc": "2.0",
26-
"id": 1,
27-
"method": "message/send",
28-
"params": {
29-
"message": {
30-
"role": "user",
31-
"parts": [{"kind": "text", "text": message_text}],
32-
"messageId": message_id,
33-
"contextId": context_id,
34-
},
35-
"metadata": {},
36-
},
37-
}
38-
39-
40-
def create_agent(
41-
name: str = "test_agent",
42-
) -> LlmAgent:
43-
return LlmAgent(
44-
name=name,
45-
model=LiteLlm(model="gemini/gemini-2.5-flash"),
46-
description="Test agent",
47-
instruction="You are a test agent.",
48-
)
49-
50-
51-
@pytest.mark.asyncio
52-
async def test_external_token_stored_in_session() -> None:
53-
"""Test that X-External-Token header is captured and stored in ADK session state."""
54-
# Given: An agent with a tool
55-
agent = create_agent()
56-
tools = [McpTool(name="test_tool", url=AnyHttpUrl("http://tool-1.local/mcp"))]
57-
test_token = "test-bearer-token-12345"
58-
59-
# When: Creating an app and sending a request with X-External-Token header
60-
rpc_url = "http://localhost:80/"
61-
app = to_a2a(
62-
agent=agent,
63-
rpc_url=rpc_url,
64-
tools=tools,
65-
agent_factory=AgentFactory(retry=Retry(total=2)),
66-
)
67-
68-
async with LifespanManager(app) as manager:
69-
client = TestClient(manager.app)
70-
71-
# Send a request with the X-External-Token header
72-
response = client.post(
73-
"",
74-
json=create_send_message_request(),
75-
headers={"X-External-Token": test_token},
76-
)
3+
from agenticlayer.agent import _get_mcp_headers_from_session
4+
from agenticlayer.constants import EXTERNAL_TOKEN_SESSION_KEY
5+
from google.adk.sessions.session import Session
776

78-
# Then: The request should succeed
79-
assert response.status_code == 200
807

81-
# Note: We cannot directly verify the session state from the test client
82-
# because the session is internal to the ADK executor. However, we can
83-
# verify that the app starts correctly and processes the request, which
84-
# means our custom executor is working.
85-
86-
87-
@pytest.mark.asyncio
88-
async def test_header_provider_retrieves_token_from_session() -> None:
8+
def test_header_provider_retrieves_token_from_session() -> None:
899
"""Test that the header provider function can retrieve token from session state."""
90-
from agenticlayer.agent import _get_mcp_headers_from_session
91-
from google.adk.sessions.session import Session
92-
9310
# Given: A session with an external token stored
9411
test_token = "test-api-token-xyz"
9512
session = Session(
9613
id="test-session",
9714
app_name="test-app",
9815
user_id="test-user",
99-
state={_EXTERNAL_TOKEN_SESSION_KEY: test_token},
16+
state={EXTERNAL_TOKEN_SESSION_KEY: test_token},
10017
events=[],
10118
last_update_time=0.0,
10219
)
@@ -115,12 +32,8 @@ def __init__(self, session):
11532
assert headers == {"X-External-Token": test_token}
11633

11734

118-
@pytest.mark.asyncio
119-
async def test_header_provider_returns_empty_when_no_token() -> None:
35+
def test_header_provider_returns_empty_when_no_token() -> None:
12036
"""Test that the header provider returns empty dict when no token is present."""
121-
from agenticlayer.agent import _get_mcp_headers_from_session
122-
from google.adk.sessions.session import Session
123-
12437
# Given: A session without an external token
12538
session = Session(
12639
id="test-session",
@@ -145,13 +58,11 @@ def __init__(self, session):
14558
assert headers == {}
14659

14760

148-
@pytest.mark.asyncio
149-
async def test_header_provider_handles_none_context() -> None:
61+
def test_header_provider_handles_none_context() -> None:
15062
"""Test that the header provider safely handles None context."""
151-
from agenticlayer.agent import _get_mcp_headers_from_session
152-
15363
# When: Calling the header provider with None
15464
headers = _get_mcp_headers_from_session(None)
15565

15666
# Then: The headers should be empty (no exception)
15767
assert headers == {}
68+

adk/tests/test_fixtures.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Common test fixtures and helper functions for ADK tests."""
2+
3+
import uuid
4+
from typing import Any
5+
6+
from google.adk.agents.llm_agent import LlmAgent
7+
from google.adk.models.lite_llm import LiteLlm
8+
9+
10+
def create_send_message_request(
11+
message_text: str = "Hello, agent!",
12+
) -> dict[str, Any]:
13+
"""Helper function to create a valid A2A send message request."""
14+
message_id = str(uuid.uuid4())
15+
context_id = str(uuid.uuid4())
16+
return {
17+
"jsonrpc": "2.0",
18+
"id": 1,
19+
"method": "message/send",
20+
"params": {
21+
"message": {
22+
"role": "user",
23+
"parts": [{"kind": "text", "text": message_text}],
24+
"messageId": message_id,
25+
"contextId": context_id,
26+
},
27+
"metadata": {},
28+
},
29+
}
30+
31+
32+
def create_agent(
33+
name: str = "test_agent",
34+
) -> LlmAgent:
35+
"""Helper function to create a test agent."""
36+
return LlmAgent(
37+
name=name,
38+
model=LiteLlm(model="gemini/gemini-2.5-flash"),
39+
description="Test agent",
40+
instruction="You are a test agent.",
41+
)

0 commit comments

Comments
 (0)