fix(deps): upgrade OpenAI stack and resolve dependency vulnerabilities

This commit is contained in:
chenmingxuan committed 2026-10-28 11:22:04 +08:00
1 parent bb873ec0e0
commit 6b2c0f199d
14 files changed
+2738 -2732

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
"""Regression coverage for the prototype's python-jose to PyJWT migration."""
from datetime import UTC, datetime, timedelta
import importlib.util
from pathlib import Path
import sys
from types import ModuleType, SimpleNamespace
from fastapi import HTTPException
from fastapi.security import HTTPAuthorizationCredentials
import jwt
import pytest
@pytest.fixture
def auth(monkeypatch):
# Load only the authentication module, without Docker, GCP or real settings.
config = ModuleType("app.config")
config.settings = SimpleNamespace(
JWT_SECRET_KEY="test-only-signing-key-with-at-least-32-bytes",
JWT_ALGORITHM="HS256",
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=60,
)
monkeypatch.setitem(sys.modules, "app.config", config)
path = (
Path(__file__).resolve().parents[2] / "playground/backend_manager/app/auth/jwt_handler.py"
)
spec = importlib.util.spec_from_file_location("backend_manager_jwt_test", path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
@pytest.mark.asyncio
async def test_signed_token_preserves_claims_and_authenticated_user(auth):
token = auth.create_access_token("user@example.com", {"role": "tester"})
claims = auth.decode_access_token(token)
assert claims["sub"] == "user@example.com"
assert claims["role"] == "tester"
assert claims["iss"] == "artemis-backend-manager"
assert claims["exp"] > claims["iat"]
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
assert await auth.get_current_user(credentials) == "user@example.com"
@pytest.mark.parametrize("invalid", ["expired", "signature", "algorithm", "unsigned", "malformed"])
def test_invalid_tokens_remain_unauthorized(auth, invalid):
claims = {"sub": "user", "exp": datetime.now(UTC) + timedelta(minutes=5)}
key = auth.settings.JWT_SECRET_KEY
algorithm = "HS256"
if invalid == "expired":
claims["exp"] = datetime.now(UTC) - timedelta(minutes=5)
elif invalid == "signature":
key = "different-test-only-signing-key-with-32-bytes"
elif invalid == "algorithm":
algorithm = "HS384"
key = "another-test-only-key-long-enough-for-hs384-signatures"
elif invalid == "unsigned":
algorithm, key = "none", None
token = jwt.encode(claims, key, algorithm=algorithm)
if invalid == "malformed":
token = "not-a-jwt"
with pytest.raises(HTTPException) as error:
auth.decode_access_token(token)
assert error.value.status_code == 401
assert error.value.headers == {"WWW-Authenticate": "Bearer"}
@pytest.mark.asyncio
async def test_missing_subject_is_rejected(auth):
token = jwt.encode({}, auth.settings.JWT_SECRET_KEY, algorithm="HS256")
credentials = HTTPAuthorizationCredentials(scheme="Bearer", credentials=token)
with pytest.raises(HTTPException) as error:
await auth.get_current_user(credentials)
assert error.value.status_code == 401
+53 -8
View File
@@ -5,7 +5,7 @@ import json
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import httpx2 as httpx
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
from langchain_core.runnables import RunnableLambda
from pydantic import BaseModel, SecretStr
@@ -59,26 +59,46 @@ def response_stream(status, arguments, name):
]
if status:
events.append({"type": "response." + status, "response": body})
return "".join("data: " + json.dumps(event) + "\n\n" for event in events) + "data: [DONE]\n\n"
return (
"".join("data: " + json.dumps(event, ensure_ascii=False) + "\n\n" for event in events)
+ "data: [DONE]\n\n"
)
class FragmentedEventStream(httpx.AsyncByteStream):
"""Split SSE frames and multibyte UTF-8 characters at transport boundaries."""
def __init__(self, text):
self.data = text.encode("utf-8")
async def __aiter__(self):
for offset in range(0, len(self.data), 4093):
yield self.data[offset : offset + 4093]
@pytest_asyncio.fixture
async def transport_case(monkeypatch, tmp_path):
async def transport_case(monkeypatch, tmp_path, request):
from langchain_openai import ChatOpenAI
import openai
requests, outputs, clients, events = [], [], [], []
def handle(request):
payload = json.loads(request.content)
def handle(http_request):
payload = json.loads(http_request.content)
requests.append(payload)
status, arguments = outputs.pop(0)
name = payload.get("tools", [{"name": "finish"}])[0]["name"]
if payload.get("stream"):
stream = response_stream(status, arguments, name)
body = (
{"stream": FragmentedEventStream(stream)}
if getattr(request, "param", None) == "fragmented"
else {"text": stream}
)
return httpx.Response(
200,
text=response_stream(status, arguments, name),
headers={"content-type": "text/event-stream"},
**body,
)
return httpx.Response(200, json=response_body(status, arguments, name))
@@ -175,15 +195,40 @@ async def test_completed_stream_returns_original_valid_arguments(transport_case)
assert result.tool_calls[0]["args"] == {"passed": True}
@pytest.mark.asyncio
@pytest.mark.parametrize("transport_case", ["fragmented"], indirect=True)
@pytest.mark.parametrize("status", ["completed", None])
async def test_large_unicode_tool_stream_requires_completion(transport_case, status):
case = transport_case
arguments = {"passed": True, "evidence": "截图已核验。\n" * 65536}
encoded = json.dumps(arguments, ensure_ascii=False)
assert len(encoded.encode("utf-8")) > 1024 * 1024
case.outputs.append((status, encoded))
if status == "completed":
result = await wrapped(case).complete([])
assert result.response_metadata["status"] == "completed"
assert result.tool_calls[0]["args"] == arguments
else:
with pytest.raises(LLMExhaustedError) as failure:
await wrapped(case).complete([])
assert failure.value.failure.category is FailureCategory.INVALID_OUTPUT
assert len(case.requests) == 1
@pytest.mark.asyncio
@pytest.mark.parametrize("recovers", [False, True])
@pytest.mark.parametrize("initial_status", [None, "failed"])
async def test_integrity_retry_is_bounded_and_keeps_existing_tool_evidence(
transport_case, recovers
transport_case, recovers, initial_status
):
case = transport_case
case.endpoint.max_retries = 5
case.outputs.extend(
[(None, '{"passed":false}'), ("completed" if recovers else None, '{"passed":true}')]
[
(initial_status, '{"passed":false}'),
("completed" if recovers else initial_status, '{"passed":true}'),
]
)
messages = [
HumanMessage(content="Inspect then verify"),