From ba74eb4fd2dc84765212acdd48e7c0b9c9b723b5 Mon Sep 17 00:00:00 2001 From: Renn F Date: Mon, 15 Jun 2026 07:54:34 +0200 Subject: [PATCH] fix(test): make test_app route assertions robust to FastAPI 0.137 _IncludedRouter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FastAPI 0.137 stopped flattening include_router into app.routes — each include is now an _IncludedRouter (a BaseRoute with no .path), so `{r.path for r in app.routes}` raised AttributeError and the two router-registration tests failed (the bump arrived via the claude-agent-sdk update in uv.lock). Add _registered_paths(): OpenAPI schema paths (the stable public contract) plus each included router's prefix, which also covers the websocket /ws mount (never in the schema). Drops the now-incorrect type: ignore[attr-defined]. --- tests/unit/api/test_app.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/unit/api/test_app.py b/tests/unit/api/test_app.py index 6077f7d9..31b13952 100644 --- a/tests/unit/api/test_app.py +++ b/tests/unit/api/test_app.py @@ -21,6 +21,27 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator +def _registered_paths(app: FastAPI) -> set[str]: + """Every registered path, robust to FastAPI 0.137+ routing internals. + + From 0.137, ``include_router`` wraps each sub-router in an ``_IncludedRouter`` + (a ``BaseRoute`` with no ``.path``) instead of flattening its routes into + ``app.routes`` — so ``{r.path for r in app.routes}`` raised AttributeError + and missed every included route. Collect HTTP paths from the OpenAPI schema + (the stable public contract) plus each included router's prefix, which also + covers websocket mounts like ``/ws`` that never appear in the schema. + """ + paths = set(app.openapi().get("paths", {})) + for r in app.routes: + path = getattr(r, "path", None) + if isinstance(path, str): + paths.add(path) + prefix = getattr(getattr(r, "include_context", None), "prefix", None) + if prefix: + paths.add(prefix) + return paths + + def test_default_app_is_a_fastapi_instance() -> None: """Importing the module yields a configured FastAPI instance.""" assert isinstance(default_app, FastAPI) @@ -37,7 +58,7 @@ def test_create_app_returns_new_instance_each_call() -> None: def test_create_app_registers_all_router_prefixes() -> None: """Every router is mounted under its expected prefix.""" app = create_app() - paths = {r.path for r in app.routes} # type: ignore[attr-defined] + paths = _registered_paths(app) # Spot-check a representative path from each prefix group. expected_prefixes = [ "/api/agents", @@ -70,7 +91,7 @@ def test_create_app_registers_all_router_prefixes() -> None: def test_create_app_includes_v1_flow_routes() -> None: """API v1 (intent-verb) routers from `routes/v1/*` are mounted.""" app = create_app() - paths = {r.path for r in app.routes} # type: ignore[attr-defined] + paths = _registered_paths(app) # v1 routers register their own prefixes; we just confirm /api/v1 paths # exist after include_router. assert any(p.startswith("/api/v1") for p in paths)