Author SHA1 Message Date
sysadmin c74b8da400 fix(webui): package-qualify webui_testclient import across test suite (Closes #682) 2026-07-24 18:18:10 -04:00
sysadmin 5deb66c7f6 fix(webui): migrate Starlette TestClient to httpx2 (#682)
Starlette 1.3.x prefers httpx2 for starlette.testclient.TestClient; plain
httpx still works but emits StarletteDeprecationWarning.

- Pin httpx2==2.9.1 (keep httpx for MCP/runtime)
- Centralize Web UI TestClient import via tests/webui_testclient.py
- Point all test_webui_* modules at the helper
- Add regression tests that the deprecation warning is gone

Closes #682
2026-07-24 17:29:16 -04:00
21 changed files with 112 additions and 18 deletions
+2
View File
@@ -9,6 +9,8 @@ cryptography==49.0.0
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
# Starlette 1.3.x TestClient prefers httpx2; plain httpx remains for MCP/runtime (#682).
httpx2==2.9.1
httpx-sse==0.4.3
idna==3.18
iniconfig==2.3.0
+72
View File
@@ -0,0 +1,72 @@
"""Starlette TestClient uses httpx2 without deprecation warning (#682)."""
from __future__ import annotations
import importlib
import sys
import unittest
import warnings
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
class TestHttpx2Installed(unittest.TestCase):
def test_httpx2_importable(self) -> None:
httpx2 = importlib.import_module("httpx2")
self.assertTrue(hasattr(httpx2, "Client") or hasattr(httpx2, "AsyncClient"))
class TestNoStarletteTestClientDeprecation(unittest.TestCase):
def test_starlette_testclient_import_does_not_warn(self) -> None:
# Force a fresh import path so a previous httpx-fallback warning cannot
# hide a regression after httpx2 is present. Restore sys.modules afterwards.
saved = {
name: sys.modules[name]
for name in list(sys.modules)
if name == "starlette.testclient" or name.startswith("starlette.testclient.")
}
try:
for name in list(saved):
del sys.modules[name]
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
import starlette.testclient as testclient # noqa: F401
dep = [
w
for w in caught
if "httpx" in str(w.message).lower()
and "deprecated" in str(w.message).lower()
]
self.assertEqual(
dep,
[],
f"expected no Starlette httpx deprecation warning; got: "
f"{[str(w.message) for w in dep]}",
)
finally:
sys.modules.update(saved)
def test_canonical_webui_testclient_import(self) -> None:
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
from tests.webui_testclient import TestClient
from webui.app import create_app
client = TestClient(create_app())
response = client.get("/")
self.assertIn(response.status_code, {200, 302, 404})
dep = [
w
for w in caught
if "httpx" in str(w.message).lower()
and "deprecated" in str(w.message).lower()
]
self.assertEqual(dep, [], [str(w.message) for w in dep])
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
import os
import tempfile
import unittest
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
import control_plane_db
from webui.analytics_loader import (
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.audit_validator import audit_report, infer_task_kind
+1 -1
View File
@@ -23,7 +23,7 @@ import sys
import tempfile
import unittest
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
+1 -1
View File
@@ -7,7 +7,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.deployment_boundary import (
+1 -1
View File
@@ -6,7 +6,7 @@ from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from task_capability_map import TASK_CAPABILITY_MAP
from webui.app import create_app
+1 -1
View File
@@ -15,7 +15,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
import control_plane_db
from webui.app import create_app
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.lease_loader import (
+1 -1
View File
@@ -9,7 +9,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from starlette.routing import Route
from webui.app import create_app
+1 -1
View File
@@ -7,7 +7,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.project_registry import (
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.prompt_library import find_prompt, library_to_dict, load_prompt_library, prompt_to_dict
+1 -1
View File
@@ -7,7 +7,7 @@ from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.queue_loader import (
+1 -1
View File
@@ -6,7 +6,7 @@ from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.runtime_health import _role_kind, load_runtime_snapshot, snapshot_to_dict
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.routing import Route
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui import layout
from webui.app import create_app
+1 -1
View File
@@ -5,7 +5,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
+1 -1
View File
@@ -16,7 +16,7 @@ from unittest import mock
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
import control_plane_db
from webui.app import create_app
+1 -1
View File
@@ -11,7 +11,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.deployment_boundary import scan_text_for_client_secrets
+1 -1
View File
@@ -13,7 +13,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
import control_plane_db
from canonical_thread_handoff import (
+1 -1
View File
@@ -7,7 +7,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from starlette.testclient import TestClient
from tests.webui_testclient import TestClient
from webui.app import create_app
from webui.worktree_scanner import (
+20
View File
@@ -0,0 +1,20 @@
"""Canonical Web UI ``TestClient`` import for the Gitea-Tools suite (#682).
Starlette 1.3+ builds ``starlette.testclient.TestClient`` on ``httpx2``.
Plain ``httpx`` still works but emits ``StarletteDeprecationWarning``.
All Web UI tests import ``TestClient`` from this module so:
* dependency intent is single-sourced (``httpx2`` in ``requirements.txt``);
* a future client-construction helper has one place to land;
* import-site audits do not need to re-scan every ``test_webui_*.py``.
Runtime MCP / FastMCP code continues to use ``httpx``; this module is
test-only and does not change production clients.
"""
from __future__ import annotations
from starlette.testclient import TestClient
__all__ = ["TestClient"]