diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec84c36f..b0f3cf8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ ## [Unreleased] +- Starlette `TestClient`의 기존 `httpx2==2.5.0` pin을 core 개발·테스트 의존성으로 승격하고, deprecated `httpx` fallback 경고 억제를 제거했습니다. - 긴 이메일·첨부 본문을 의미 단위 청크로 임베딩한 뒤 기존 email/attachment 벡터 계약으로 평균화하고, 청크 요청·벡터 누적을 제한된 창으로 처리합니다. OpenAI `text-embedding-3-*`에는 저장 차원(`1536`)을 직접 요청하도록 보강했습니다. 합성 메일 fixture 5건(70청크)과 provider 요청 계약으로 1,536차원 벡터 경로를 검증했으며, 실행 시 선택한 임베딩 제공자에 본문·파싱된 첨부 텍스트를 전송할 수 있습니다. 회사 기밀 데이터는 fixture·commit·PR·log에 포함하지 않습니다. - EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다. diff --git a/backend/pyproject.toml b/backend/pyproject.toml index c156d9776..046829400 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ [dependency-groups] dev = [ "coverage==7.15.1", + "httpx2==2.5.0", "pytest==9.1.1", "pytest-asyncio==1.4.0", "ruff==0.15.21", diff --git a/backend/pytest.ini b/backend/pytest.ini index 4e58599ac..c57a00260 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -1,7 +1,6 @@ [pytest] asyncio_default_fixture_loop_scope = function filterwarnings = - ignore:Using `httpx` with `starlette.testclient` is deprecated.*:starlette.exceptions.StarletteDeprecationWarning ignore:You are using a Python version.*which Google will stop supporting.*:FutureWarning ignore:Unclosed None frontend_lock = yaml.safe_load(read_repo_text("frontend/pnpm-lock.yaml")) assert backend_pins["cryptography"] == "50.0.0" + assert backend_pins["httpx2"] == "2.5.0" assert backend_pins["protobuf"] == "7.35.1" assert "cryptography==50.0.0" in backend_records + assert "httpx2==2.5.0" in backend_records assert "protobuf==7.35.1" in backend_records assert all( re.fullmatch(r"[0-9a-f]{64}", digest) - for pin in ("cryptography==50.0.0", "protobuf==7.35.1") + for pin in ( + "cryptography==50.0.0", + "httpx2==2.5.0", + "protobuf==7.35.1", + ) for digest in backend_records[pin] ) + pytest_config = read_repo_text("backend/pytest.ini") + assert "Using `httpx` with `starlette.testclient` is deprecated" not in pytest_config assert strix_pins["cryptography"] == "50.0.0" assert strix_pins["protobuf"] == "6.33.6" @@ -115,7 +123,6 @@ def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None for pin in ("cryptography==50.0.0", "protobuf==6.33.6") for digest in strix_records[pin] ) - root_importer = frontend_lock["importers"]["."] postcss_resolution = importer_resolution( root_importer, "devDependencies", "postcss" @@ -144,3 +151,10 @@ def test_container_provenance_dependency_pins_match_reviewed_manifests() -> None "undici@8.9.0", ): assert exact_lock_entry in package_records + + +def test_starlette_testclient_uses_httpx2_runtime() -> None: + """Exercise Starlette's preferred TestClient transport dependency.""" + from starlette import testclient + + assert testclient.httpx.__name__ == "httpx2" diff --git a/backend/uv.lock b/backend/uv.lock index 2f0d04a76..d455f2a61 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -683,6 +683,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/06/5c12df521b5322fb1114a83d46911b2fbcb8855ddb3a635f11c01a214af5/httpcore2-2.5.0.tar.gz", hash = "sha256:88aa170137c17328d5ac44234f9fd10706466d5fb347f3edac4d39b91137b09d", size = 64808, upload-time = "2026-06-25T14:16:56.472Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/a1/7564199d1a8728fe737b0a72e5b3f8d92dfe085a74ddf7cdd83bce5f206d/httpcore2-2.5.0-py3-none-any.whl", hash = "sha256:5ce35188de461d31e8d000bfb8ef8bf22c6c16587a211e5571deaa5e9bdf842a", size = 80330, upload-time = "2026-06-25T14:16:53.634Z" }, +] + [[package]] name = "httplib2" version = "0.32.0" @@ -710,6 +723,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "httpx2" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/e2/b5dedc0cf35aa65de5f541ccd30d2bc1fd7f1d43c9ab09f8ed9a7342317b/httpx2-2.5.0.tar.gz", hash = "sha256:e2df9cb4611021527ff8a675b1c320b610a2ec397acc8d6fe6e91df2d9b33c29", size = 83121, upload-time = "2026-06-25T14:16:57.491Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/22/859d8252dad9bc9adee34b52e62cde621ece07b042ccb2ab4da1be46695f/httpx2-2.5.0-py3-none-any.whl", hash = "sha256:3d2d4d9cf4b61f1a1f46a95947cfdb47e80cb56a2f91c6256ac8f58e4891df41", size = 76652, upload-time = "2026-06-25T14:16:55.23Z" }, +] + [[package]] name = "icalendar" version = "7.2.0" @@ -1019,6 +1048,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "coverage" }, + { name = "httpx2" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -1065,6 +1095,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "coverage", specifier = "==7.15.1" }, + { name = "httpx2", specifier = "==2.5.0" }, { name = "pytest", specifier = "==9.1.1" }, { name = "pytest-asyncio", specifier = "==1.4.0" }, { name = "ruff", specifier = "==0.15.21" }, @@ -1972,6 +2003,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" diff --git a/docs/doctoring/starlette-httpx2-testclient-dependency.md b/docs/doctoring/starlette-httpx2-testclient-dependency.md new file mode 100644 index 000000000..c66b73990 --- /dev/null +++ b/docs/doctoring/starlette-httpx2-testclient-dependency.md @@ -0,0 +1,48 @@ +# Starlette TestClient `httpx2` dependency + +## Observed failure + +Protected `develop@042b0c70531b229af3acbd0421a2f23098d848b3` pins Starlette +1.3.1 but did not install `httpx2`. Importing `starlette.testclient` therefore +fell back to deprecated `httpx`; warning-as-error test runs stopped during +collection. Removing the warning filter without installing the preferred +transport would expose the defect without repairing it. + +## Decision and boundary + +Pin `httpx2==2.5.0` in the repository's existing combined backend +development/direct-test manifests and immutable locks. Keep application HTTP +clients on their existing `httpx` path. A runtime regression test imports +Starlette's TestClient module and verifies that its selected transport module is +`httpx2`; manifest and digest checks alone are insufficient evidence. + +Starlette 1.2.0 introduced TestClient support for `httpx2`, and 1.3.0 added it +to the `full` extra. The 2.5.0 wheel in this change matches PyPI's published +SHA-256 digest `3d2d4d9cf4b61f1a1f46a95947cfdb47e80cb56a2f91c6256ac8f58e4891df41`. +PyPI records a trusted-publishing attestation from the `pydantic/httpx2` +repository at tag `v2.5.0`. These facts establish origin and integrity; they do +not transfer current-head CI or protected-merge authority. + +## Verification and rollback + +Run from `backend/`: + +```bash +uv run --frozen pytest -q -W error tests/test_container_dependency_pin_contract.py +uv run --frozen ruff check tests/test_container_dependency_pin_contract.py +``` + +Rollback removes the direct pin, regenerated lock records, runtime assertion, +and obsolete-warning-filter removal together. Do not restore only the warning +suppression. + +## References + +Kludex. (2026). *Starlette release notes*. GitHub. +https://github.com/Kludex/starlette/blob/main/docs/release-notes.md + +Python Packaging Authority. (2026). *httpx2 2.5.0 file details and provenance*. +PyPI. https://pypi.org/project/httpx2/2.5.0/ + +Pydantic. (2026). *HTTPX2 v2.5.0* [Source code]. GitHub. +https://github.com/pydantic/httpx2/tree/v2.5.0