Skip to content

Commit e369ea0

Browse files
test(agentkit): unit tests for apps entrypoints + deploy-pipeline (surface + core logic) (#105)
2 parents ad13eaf + 48e00c6 commit e369ea0

18 files changed

Lines changed: 7785 additions & 0 deletions

tests/apps/test_a2a_app.py

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Offline unit guards for ``AgentkitA2aApp``.
16+
17+
These tests exercise the decorator guards (``agent_executor`` / ``task_store`` /
18+
``ping``), the pure ``_format_ping_status`` helper, the async ``ping_endpoint``
19+
handler (driven via ``asyncio.run``), and the ``/env`` runtime-detection route
20+
(driven via ``starlette.testclient.TestClient``) -- all without building the
21+
real A2A server or ever calling ``run()`` (which binds sockets via uvicorn).
22+
"""
23+
24+
from __future__ import annotations
25+
26+
import asyncio
27+
import json
28+
29+
import pytest
30+
from a2a.server.agent_execution import AgentExecutor
31+
from a2a.server.tasks.task_store import TaskStore
32+
from starlette.applications import Starlette
33+
from starlette.testclient import TestClient
34+
35+
from agentkit.apps.a2a_app.a2a_app import AgentkitA2aApp
36+
37+
38+
# ---------------------------------------------------------------------------
39+
# Minimal concrete a2a subclasses used as decorator targets.
40+
# a2a's AgentExecutor requires execute/cancel; TaskStore requires get/save/delete.
41+
# ---------------------------------------------------------------------------
42+
43+
44+
class _FakeAgentExecutor(AgentExecutor):
45+
async def execute(self, context, event_queue): # pragma: no cover - never invoked
46+
return None
47+
48+
async def cancel(self, context, event_queue): # pragma: no cover - never invoked
49+
return None
50+
51+
52+
class _FakeTaskStore(TaskStore):
53+
async def get(self, task_id): # pragma: no cover - never invoked
54+
return None
55+
56+
async def save(self, task): # pragma: no cover - never invoked
57+
return None
58+
59+
async def delete(self, task_id): # pragma: no cover - never invoked
60+
return None
61+
62+
63+
class _NotAnExecutor:
64+
"""A plain class that is deliberately NOT a subclass of AgentExecutor."""
65+
66+
67+
class _NotATaskStore:
68+
"""A plain class that is deliberately NOT a subclass of TaskStore."""
69+
70+
71+
class _DummyRequest:
72+
"""Placeholder request object; ping_endpoint never reads the request."""
73+
74+
75+
# ---------------------------------------------------------------------------
76+
# _format_ping_status (pure)
77+
# ---------------------------------------------------------------------------
78+
79+
80+
def test_format_ping_status_wraps_string_result_under_status_key():
81+
app = AgentkitA2aApp()
82+
assert app._format_ping_status("ok") == {"status": "ok"}
83+
84+
85+
def test_format_ping_status_wraps_dict_result_under_status_key():
86+
app = AgentkitA2aApp()
87+
payload = {"healthy": True, "detail": "fine"}
88+
assert app._format_ping_status(payload) == {"status": payload}
89+
90+
91+
def test_format_ping_status_returns_error_dict_for_non_str_non_dict_result():
92+
app = AgentkitA2aApp()
93+
assert app._format_ping_status(12345) == {
94+
"status": "error",
95+
"message": "Invalid response type.",
96+
}
97+
98+
99+
def test_format_ping_status_treats_bool_as_invalid_response_type():
100+
# NOTE: bool is not str/dict, so it falls through to the error branch.
101+
app = AgentkitA2aApp()
102+
assert app._format_ping_status(True) == {
103+
"status": "error",
104+
"message": "Invalid response type.",
105+
}
106+
107+
108+
# ---------------------------------------------------------------------------
109+
# ping() decorator guard
110+
# ---------------------------------------------------------------------------
111+
112+
113+
def test_ping_decorator_rejects_function_with_parameters():
114+
app = AgentkitA2aApp()
115+
116+
def health(request):
117+
return "ok"
118+
119+
with pytest.raises(AssertionError):
120+
app.ping(health)
121+
assert app._ping_func is None
122+
123+
124+
def test_ping_decorator_registers_zero_argument_function_and_returns_it():
125+
app = AgentkitA2aApp()
126+
127+
def health():
128+
return "ok"
129+
130+
returned = app.ping(health)
131+
assert returned is health
132+
assert app._ping_func is health
133+
134+
135+
# ---------------------------------------------------------------------------
136+
# ping_endpoint (async, driven via asyncio.run)
137+
# ---------------------------------------------------------------------------
138+
139+
140+
def test_ping_endpoint_returns_404_when_no_ping_func_registered():
141+
app = AgentkitA2aApp()
142+
response = asyncio.run(app.ping_endpoint(_DummyRequest()))
143+
assert response.status_code == 404
144+
145+
146+
def test_ping_endpoint_wraps_sync_ping_func_result_under_status():
147+
app = AgentkitA2aApp()
148+
app._ping_func = lambda: "alive"
149+
150+
response = asyncio.run(app.ping_endpoint(_DummyRequest()))
151+
152+
assert response.status_code == 200
153+
assert json.loads(bytes(response.body)) == {"status": "alive"}
154+
155+
156+
def test_ping_endpoint_awaits_coroutine_ping_func_result():
157+
app = AgentkitA2aApp()
158+
159+
async def health():
160+
return {"db": "up"}
161+
162+
app._ping_func = health
163+
164+
response = asyncio.run(app.ping_endpoint(_DummyRequest()))
165+
166+
assert response.status_code == 200
167+
assert json.loads(bytes(response.body)) == {"status": {"db": "up"}}
168+
169+
170+
def test_ping_endpoint_returns_500_error_payload_when_ping_func_raises():
171+
app = AgentkitA2aApp()
172+
173+
def boom():
174+
raise ValueError("kaboom")
175+
176+
app._ping_func = boom
177+
178+
response = asyncio.run(app.ping_endpoint(_DummyRequest()))
179+
180+
assert response.status_code == 500
181+
assert json.loads(bytes(response.body)) == {
182+
"status": "error",
183+
"message": "kaboom",
184+
}
185+
186+
187+
def test_ping_endpoint_returns_error_payload_when_ping_func_returns_invalid_type():
188+
app = AgentkitA2aApp()
189+
app._ping_func = lambda: 42
190+
191+
response = asyncio.run(app.ping_endpoint(_DummyRequest()))
192+
193+
assert response.status_code == 200
194+
assert json.loads(bytes(response.body)) == {
195+
"status": "error",
196+
"message": "Invalid response type.",
197+
}
198+
199+
200+
# ---------------------------------------------------------------------------
201+
# agent_executor() decorator guard
202+
# ---------------------------------------------------------------------------
203+
204+
205+
def test_agent_executor_rejects_class_not_subclassing_a2a_agent_executor():
206+
app = AgentkitA2aApp()
207+
208+
with pytest.raises(TypeError):
209+
app.agent_executor()(_NotAnExecutor)
210+
assert app._agent_executor is None
211+
212+
213+
def test_agent_executor_binds_valid_executor_and_returns_the_class():
214+
app = AgentkitA2aApp()
215+
216+
class _MyExecutor(_FakeAgentExecutor):
217+
pass
218+
219+
returned = app.agent_executor()(_MyExecutor)
220+
221+
assert returned is _MyExecutor
222+
assert isinstance(app._agent_executor, _MyExecutor)
223+
224+
225+
def test_agent_executor_raises_runtime_error_when_binding_a_second_executor():
226+
app = AgentkitA2aApp()
227+
228+
class _First(_FakeAgentExecutor):
229+
pass
230+
231+
class _Second(_FakeAgentExecutor):
232+
pass
233+
234+
app.agent_executor()(_First)
235+
with pytest.raises(RuntimeError):
236+
app.agent_executor()(_Second)
237+
238+
239+
# ---------------------------------------------------------------------------
240+
# task_store() decorator guard
241+
# ---------------------------------------------------------------------------
242+
243+
244+
def test_task_store_rejects_class_not_subclassing_a2a_task_store():
245+
app = AgentkitA2aApp()
246+
247+
with pytest.raises(TypeError):
248+
app.task_store()(_NotATaskStore)
249+
assert app._task_store is None
250+
251+
252+
def test_task_store_binds_valid_store_and_returns_the_class():
253+
app = AgentkitA2aApp()
254+
255+
class _MyStore(_FakeTaskStore):
256+
pass
257+
258+
returned = app.task_store()(_MyStore)
259+
260+
assert returned is _MyStore
261+
assert isinstance(app._task_store, _MyStore)
262+
263+
264+
def test_task_store_raises_runtime_error_when_binding_a_second_store():
265+
app = AgentkitA2aApp()
266+
267+
class _First(_FakeTaskStore):
268+
pass
269+
270+
class _Second(_FakeTaskStore):
271+
pass
272+
273+
app.task_store()(_First)
274+
with pytest.raises(RuntimeError):
275+
app.task_store()(_Second)
276+
277+
278+
# ---------------------------------------------------------------------------
279+
# add_env_detect_route() -> GET /env
280+
# ---------------------------------------------------------------------------
281+
282+
283+
def test_env_route_reports_agentkit_when_runtime_iam_role_trn_is_set(monkeypatch):
284+
monkeypatch.setenv("RUNTIME_IAM_ROLE_TRN", "x")
285+
286+
bare_app = Starlette()
287+
AgentkitA2aApp().add_env_detect_route(bare_app)
288+
289+
with TestClient(bare_app) as client:
290+
response = client.get("/env")
291+
292+
assert response.status_code == 200
293+
assert response.json() == {"env": "agentkit"}
294+
295+
296+
def test_env_route_reports_veadk_when_runtime_iam_role_trn_is_absent(monkeypatch):
297+
monkeypatch.delenv("RUNTIME_IAM_ROLE_TRN", raising=False)
298+
299+
bare_app = Starlette()
300+
AgentkitA2aApp().add_env_detect_route(bare_app)
301+
302+
with TestClient(bare_app) as client:
303+
response = client.get("/env")
304+
305+
assert response.status_code == 200
306+
assert response.json() == {"env": "veadk"}

0 commit comments

Comments
 (0)