Skip to content

Commit 7fe7a87

Browse files
committed
fixup! feat(testing): add build_test_client async context manager
Address pre-PR review findings: use Sequence[str] for allowed_hosts, fix confusing docstring sentence, strengthen headers test assertion, drop redundant asyncio markers, add ImportError test for missing dep. https://claude.ai/code/session_01UZyqrAkoJPCEz5VLerzxFv
1 parent 032bf72 commit 7fe7a87

2 files changed

Lines changed: 20 additions & 14 deletions

File tree

src/adcp/testing/decisioning.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
from adcp.decisioning.types import Account
3535

3636
if TYPE_CHECKING:
37-
from collections.abc import AsyncIterator, Mapping
37+
from collections.abc import AsyncIterator, Mapping, Sequence
3838
from datetime import datetime
3939

4040
import httpx
@@ -135,7 +135,7 @@ def build_asgi_app(
135135
name: str | None = None,
136136
advertise_all: bool = False,
137137
auto_emit_completion_webhooks: bool = False,
138-
allowed_hosts: list[str] | None = None,
138+
allowed_hosts: Sequence[str] | None = None,
139139
**factory_kwargs: Any,
140140
) -> Any:
141141
"""Build a Starlette ASGI app for in-process integration tests.
@@ -218,9 +218,9 @@ async def build_test_client(
218218
resp = await client.post("/mcp/", json=...)
219219
220220
The context manager starts the ASGI lifespan on entry and shuts down both
221-
the client and the lifespan manager on exit. The yielded
222-
``httpx.AsyncClient`` is an :class:`contextlib.AbstractAsyncContextManager`
223-
result; callers use it directly as an async HTTP client.
221+
the client and the lifespan manager on exit. ``build_test_client(...)``
222+
itself is an ``AbstractAsyncContextManager[httpx.AsyncClient]``; the
223+
yielded object is a plain ``httpx.AsyncClient``.
224224
225225
Requires ``asgi-lifespan`` (included in ``adcp[dev]``). Raises
226226
:class:`ImportError` with an actionable message if it is not installed.

tests/test_testing_decisioning.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -196,31 +196,27 @@ def test_build_asgi_app_forwards_allowed_hosts() -> None:
196196
# ---- build_test_client ----
197197

198198

199-
@pytest.mark.asyncio
200199
async def test_build_test_client_yields_httpx_async_client() -> None:
201200
"""The context manager yields an ``httpx.AsyncClient`` instance."""
202201
platform = _SalesPlatformWithMethods()
203202
async with build_test_client(platform) as client:
204203
assert isinstance(client, httpx.AsyncClient)
205204

206205

207-
@pytest.mark.asyncio
208206
async def test_build_test_client_default_base_url() -> None:
209207
"""Default ``base_url="http://test"`` is used when not overridden."""
210208
platform = _SalesPlatformWithMethods()
211209
async with build_test_client(platform) as client:
212210
assert str(client.base_url) == "http://test"
213211

214212

215-
@pytest.mark.asyncio
216213
async def test_build_test_client_custom_base_url() -> None:
217214
"""``base_url`` override is forwarded to the client."""
218215
platform = _SalesPlatformWithMethods()
219216
async with build_test_client(platform, base_url="http://localhost") as client:
220217
assert str(client.base_url) == "http://localhost"
221218

222219

223-
@pytest.mark.asyncio
224220
async def test_build_test_client_can_make_request() -> None:
225221
"""The yielded client can actually reach the mounted MCP endpoint."""
226222
platform = _SalesPlatformWithMethods()
@@ -245,27 +241,37 @@ async def test_build_test_client_can_make_request() -> None:
245241
assert resp.status_code == 200
246242

247243

248-
@pytest.mark.asyncio
249244
async def test_build_test_client_headers_kwarg() -> None:
250-
"""Default ``headers=`` are passed through to the client."""
245+
"""Default ``headers=`` are attached to the client — not silently dropped."""
251246
platform = _SalesPlatformWithMethods()
252247
async with build_test_client(
253248
platform, headers={"x-custom": "value"}
254249
) as client:
255-
assert isinstance(client, httpx.AsyncClient)
250+
assert "x-custom" in dict(client.headers)
256251

257252

258-
@pytest.mark.asyncio
259253
async def test_build_test_client_follow_redirects_default_true() -> None:
260254
"""``follow_redirects`` defaults to ``True`` on the yielded client."""
261255
platform = _SalesPlatformWithMethods()
262256
async with build_test_client(platform) as client:
263257
assert client.follow_redirects is True
264258

265259

266-
@pytest.mark.asyncio
267260
async def test_build_test_client_follow_redirects_override() -> None:
268261
"""``follow_redirects=False`` is respected."""
269262
platform = _SalesPlatformWithMethods()
270263
async with build_test_client(platform, follow_redirects=False) as client:
271264
assert client.follow_redirects is False
265+
266+
267+
def test_build_test_client_raises_import_error_without_asgi_lifespan() -> None:
268+
"""Missing ``asgi-lifespan`` raises ``ImportError`` with an actionable message."""
269+
import sys
270+
import unittest.mock
271+
272+
platform = _SalesPlatformWithMethods()
273+
with unittest.mock.patch.dict(sys.modules, {"asgi_lifespan": None}):
274+
with pytest.raises(ImportError, match="asgi-lifespan is required"):
275+
import asyncio
276+
277+
asyncio.run(build_test_client(platform).__aenter__())

0 commit comments

Comments
 (0)