feat: 扩展 Rust gateway 全功能模块,新增 billing/crypto/wallet crate 及完整数据层

- 新增 aether-billing、aether-crypto、aether-wallet 独立 crate
- aether-data 扩展 repository 层:announcements、auth_modules、billing、
  candidate_selection、gemini_file_mappings、global_models、management_tokens、
  oauth_providers、proxy_nodes、quota、users、wallet 等模块
- aether-gateway 新增 api/auth/billing/control/middleware/scheduler/usage/
  video_tasks/hooks/maintenance/model_fetch/provider_transport 等功能模块
- 重构 executor decision 和 gateway state 为模块目录结构
- 新增 gateway router、frontdoor 路由层及对应测试
- Python 侧 API 路由重构,新增 compat/support 模块
- 前端 Logo 组件更新及 Provider 管理页面调整
This commit is contained in:
fawney19
2026-03-31 19:19:04 +08:00
parent b5a0070023
commit ddf18fed9a
690 changed files with 235087 additions and 16301 deletions

View File

@@ -0,0 +1,235 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _normalize_state(adapter: Any) -> dict[str, Any]:
state: dict[str, Any] = {}
for key, value in dict(getattr(adapter, "__dict__", {})).items():
if hasattr(value, "model_dump"):
state[key] = value.model_dump()
elif hasattr(value, "filename"):
state[key] = {
"filename": value.filename,
"content_type": getattr(value, "content_type", None),
}
else:
state[key] = value
return state
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.admin import gemini_files as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": _normalize_state(adapter),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_admin_gemini_files_list_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"items": [], "total": 0, "page": 2, "page_size": 50},
)
response = client.get(
"/api/admin/gemini-files/mappings?page=2&page_size=50&include_expired=true&search=demo"
)
assert response.status_code == 200
assert response.json() == {"items": [], "total": 0, "page": 2, "page_size": 50}
assert calls == [
{
"adapter_type": "AdminGeminiFilesListMappingsAdapter",
"mode": "admin",
"adapter_state": {
"page": 2,
"page_size": 50,
"include_expired": True,
"search": "demo",
},
}
]
def test_admin_gemini_files_stats_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={
"total_mappings": 1,
"active_mappings": 1,
"expired_mappings": 0,
"by_mime_type": {"text/plain": 1},
"capable_keys_count": 2,
},
)
response = client.get("/api/admin/gemini-files/stats")
assert response.status_code == 200
assert response.json() == {
"total_mappings": 1,
"active_mappings": 1,
"expired_mappings": 0,
"by_mime_type": {"text/plain": 1},
"capable_keys_count": 2,
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesStatsAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_admin_gemini_files_delete_mapping_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"message": "Mapping deleted successfully", "file_name": "files/1"},
)
response = client.delete("/api/admin/gemini-files/mappings/mapping-1")
assert response.status_code == 200
assert response.json() == {
"message": "Mapping deleted successfully",
"file_name": "files/1",
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesDeleteMappingAdapter",
"mode": "admin",
"adapter_state": {"mapping_id": "mapping-1"},
}
]
def test_admin_gemini_files_cleanup_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"message": "Cleaned up 3 expired mappings", "deleted_count": 3},
)
response = client.delete("/api/admin/gemini-files/mappings")
assert response.status_code == 200
assert response.json() == {
"message": "Cleaned up 3 expired mappings",
"deleted_count": 3,
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesCleanupMappingsAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_admin_gemini_files_capable_keys_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result=[{"id": "key-1", "name": "Key 1", "provider_name": "Gemini"}],
)
response = client.get("/api/admin/gemini-files/capable-keys")
assert response.status_code == 200
assert response.json() == [{"id": "key-1", "name": "Key 1", "provider_name": "Gemini"}]
assert calls == [
{
"adapter_type": "AdminGeminiFilesCapableKeysAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_admin_gemini_files_upload_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={
"display_name": "example.txt",
"mime_type": "text/plain",
"size_bytes": 5,
"results": [],
"success_count": 0,
"fail_count": 0,
},
)
response = client.post(
"/api/admin/gemini-files/upload?key_ids=key-1,key-2",
files={"file": ("example.txt", b"hello", "text/plain")},
)
assert response.status_code == 200
assert response.json() == {
"display_name": "example.txt",
"mime_type": "text/plain",
"size_bytes": 5,
"results": [],
"success_count": 0,
"fail_count": 0,
}
assert calls == [
{
"adapter_type": "AdminGeminiFilesUploadAdapter",
"mode": "admin",
"adapter_state": {
"file": {
"filename": "example.txt",
"content_type": "text/plain",
},
"key_ids": "key-1,key-2",
},
}
]

View File

@@ -0,0 +1,164 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
import pytest
@pytest.mark.asyncio
@pytest.mark.parametrize(
("route_name", "expected_adapter", "expected_state"),
[
("supported_types", "AdminProviderOAuthUnavailableAdapter", {"operation": "supported_types"}),
("start_oauth", "AdminProviderOAuthUnavailableAdapter", {"operation": "start_oauth"}),
("complete_oauth", "AdminProviderOAuthUnavailableAdapter", {"operation": "complete_oauth"}),
("refresh_oauth", "AdminProviderOAuthUnavailableAdapter", {"operation": "refresh_oauth"}),
(
"start_provider_oauth",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "start_provider_oauth"},
),
(
"complete_provider_oauth",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "complete_provider_oauth"},
),
(
"import_refresh_token",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "import_refresh_token"},
),
(
"batch_import_oauth",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "batch_import_oauth"},
),
(
"start_batch_import_oauth_task",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "start_batch_import_oauth_task"},
),
(
"get_batch_import_oauth_task_status",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "get_batch_import_oauth_task_status"},
),
(
"device_authorize",
"AdminProviderOAuthUnavailableAdapter",
{"operation": "device_authorize"},
),
("device_poll", "AdminProviderOAuthUnavailableAdapter", {"operation": "device_poll"}),
],
)
async def test_admin_provider_oauth_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
route_name: str,
expected_adapter: str,
expected_state: dict[str, Any],
) -> None:
from src.api.admin import provider_oauth as mod
captured: dict[str, Any] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{
"adapter": adapter,
"request": http_request,
"db": db,
"mode": mode,
}
)
if route_name == "supported_types":
return []
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
request = SimpleNamespace(state=SimpleNamespace())
db = object()
if route_name == "supported_types":
result = await mod.supported_types(request=request, db=db, _=None)
elif route_name == "start_oauth":
result = await mod.start_oauth("key_1", request=request, db=db, _=None)
elif route_name == "complete_oauth":
result = await mod.complete_oauth(
"key_1",
mod.CompleteOAuthRequest(callback_url="http://localhost/?code=x&state=y"),
request=request,
db=db,
_=None,
)
elif route_name == "refresh_oauth":
result = await mod.refresh_oauth("key_1", request=request, db=db, _=None)
elif route_name == "start_provider_oauth":
result = await mod.start_provider_oauth("provider_1", request=request, db=db, _=None)
elif route_name == "complete_provider_oauth":
result = await mod.complete_provider_oauth(
"provider_1",
mod.ProviderCompleteOAuthRequest(callback_url="http://localhost/?code=x&state=y"),
request=request,
db=db,
_=None,
)
elif route_name == "import_refresh_token":
result = await mod.import_refresh_token(
"provider_1",
mod.ImportRefreshTokenRequest(refresh_token="refresh-token"),
request=request,
db=db,
_=None,
)
elif route_name == "batch_import_oauth":
result = await mod.batch_import_oauth(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=request,
db=db,
_=None,
)
elif route_name == "start_batch_import_oauth_task":
result = await mod.start_batch_import_oauth_task(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=request,
db=db,
_=None,
)
elif route_name == "get_batch_import_oauth_task_status":
result = await mod.get_batch_import_oauth_task_status(
"provider_1",
"task_1",
request=request,
db=db,
_=None,
)
elif route_name == "device_authorize":
result = await mod.device_authorize(
"provider_1",
mod.DeviceAuthorizeRequest(),
request=request,
db=db,
_=None,
)
else:
result = await mod.device_poll(
"provider_1",
mod.DevicePollRequest(session_id="session_1"),
request=request,
db=db,
_=None,
)
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
assert type(captured["adapter"]).__name__ == expected_adapter
assert getattr(captured["adapter"], "__dict__", {}) == expected_state
if route_name == "supported_types":
assert result == []
else:
assert result == {"ok": True}

View File

@@ -0,0 +1,410 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _normalize_state(adapter: Any) -> dict[str, Any]:
state: dict[str, Any] = {}
for key, value in dict(getattr(adapter, "__dict__", {})).items():
if hasattr(value, "model_dump"):
state[key] = value.model_dump()
else:
state[key] = value
return state
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.admin.provider_ops import routes as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": _normalize_state(adapter),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def _architecture_payload() -> dict[str, Any]:
return {
"architecture_id": "generic_api",
"display_name": "Generic API",
"description": "generic",
"credentials_schema": {},
"supported_auth_types": [],
"supported_actions": [],
"default_connector": None,
}
def _status_payload() -> dict[str, Any]:
return {
"provider_id": "provider-1",
"is_configured": True,
"architecture_id": "generic_api",
"connection_status": {
"status": "connected",
"auth_type": "api_key",
"connected_at": None,
"expires_at": None,
"last_error": None,
},
"enabled_actions": ["balance"],
}
def _config_payload() -> dict[str, Any]:
return {
"provider_id": "provider-1",
"is_configured": True,
"architecture_id": "generic_api",
"base_url": "https://example.com",
"connector": {
"auth_type": "api_key",
"config": {},
"credentials": {},
},
}
def _verify_payload() -> dict[str, Any]:
return {
"success": True,
"message": "ok",
"data": {"verified": True},
"updated_credentials": {"token": "masked"},
}
def _action_payload() -> dict[str, Any]:
return {
"status": "success",
"action_type": "balance",
"data": {"balance": "1.23"},
"message": "ok",
"executed_at": "2026-03-26T00:00:00+00:00",
"response_time_ms": 12,
"cache_ttl_seconds": 60,
}
def test_provider_ops_architectures_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=[_architecture_payload()])
response = client.get("/api/admin/provider-ops/architectures")
assert response.status_code == 200
assert response.json() == [_architecture_payload()]
assert calls == [
{
"adapter_type": "AdminProviderOpsListArchitecturesAdapter",
"mode": "admin",
"adapter_state": {},
}
]
def test_provider_ops_architecture_detail_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_architecture_payload())
response = client.get("/api/admin/provider-ops/architectures/generic_api")
assert response.status_code == 200
assert response.json() == _architecture_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsGetArchitectureAdapter",
"mode": "admin",
"adapter_state": {"architecture_id": "generic_api"},
}
]
def test_provider_ops_status_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_status_payload())
response = client.get("/api/admin/provider-ops/providers/provider-1/status")
assert response.status_code == 200
assert response.json() == _status_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsStatusAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_config_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_config_payload())
response = client.get("/api/admin/provider-ops/providers/provider-1/config")
assert response.status_code == 200
assert response.json() == _config_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsConfigAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_save_config_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
payload = {
"architecture_id": "generic_api",
"base_url": "https://example.com",
"connector": {
"auth_type": "api_key",
"config": {"region": "us"},
"credentials": {"api_key": "secret"},
},
"actions": {"balance": {"enabled": True, "config": {"refresh": True}}},
"schedule": {"balance": "0 * * * *"},
}
response = client.put("/api/admin/provider-ops/providers/provider-1/config", json=payload)
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsSaveConfigAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"payload": payload,
},
}
]
def test_provider_ops_verify_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_verify_payload())
payload = {
"architecture_id": "generic_api",
"base_url": "https://example.com",
"connector": {
"auth_type": "api_key",
"config": {},
"credentials": {"api_key": "secret"},
},
"actions": {},
"schedule": {},
}
response = client.post("/api/admin/provider-ops/providers/provider-1/verify", json=payload)
assert response.status_code == 200
assert response.json() == _verify_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsVerifyAuthAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"payload": payload,
},
}
]
def test_provider_ops_delete_config_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
response = client.delete("/api/admin/provider-ops/providers/provider-1/config")
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsDeleteConfigAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_connect_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
response = client.post(
"/api/admin/provider-ops/providers/provider-1/connect",
json={"credentials": {"api_key": "secret"}},
)
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsConnectAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"payload": {"credentials": {"api_key": "secret"}},
},
}
]
def test_provider_ops_disconnect_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"success": True, "message": "ok"})
response = client.post("/api/admin/provider-ops/providers/provider-1/disconnect")
assert response.status_code == 200
assert response.json() == {"success": True, "message": "ok"}
assert calls == [
{
"adapter_type": "AdminProviderOpsDisconnectAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_execute_action_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.post(
"/api/admin/provider-ops/providers/provider-1/actions/balance",
json={"config": {"refresh": True}},
)
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsExecuteActionAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"action_type": "balance",
"payload": {"config": {"refresh": True}},
},
}
]
def test_provider_ops_get_balance_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.get("/api/admin/provider-ops/providers/provider-1/balance?refresh=false")
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsGetBalanceAdapter",
"mode": "admin",
"adapter_state": {
"provider_id": "provider-1",
"refresh": False,
},
}
]
def test_provider_ops_refresh_balance_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.post("/api/admin/provider-ops/providers/provider-1/balance")
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsRefreshBalanceAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_checkin_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result=_action_payload())
response = client.post("/api/admin/provider-ops/providers/provider-1/checkin")
assert response.status_code == 200
assert response.json() == _action_payload()
assert calls == [
{
"adapter_type": "AdminProviderOpsCheckinAdapter",
"mode": "admin",
"adapter_state": {"provider_id": "provider-1"},
}
]
def test_provider_ops_batch_balance_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
pipeline_result={"provider-1": _action_payload(), "provider-2": _action_payload()},
)
response = client.post(
"/api/admin/provider-ops/batch/balance?provider_ids=provider-1&provider_ids=provider-2"
)
assert response.status_code == 200
assert response.json() == {"provider-1": _action_payload(), "provider-2": _action_payload()}
assert calls == [
{
"adapter_type": "AdminProviderOpsBatchBalanceAdapter",
"mode": "admin",
"adapter_state": {"provider_ids": ["provider-1", "provider-2"]},
}
]

View File

@@ -0,0 +1,259 @@
from __future__ import annotations
from io import BytesIO
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from fastapi import HTTPException
from fastapi.responses import JSONResponse
from starlette.datastructures import UploadFile
class _FakeQuery:
def __init__(self, result: object) -> None:
self._result = result
def filter(self, *_args: object, **_kwargs: object) -> "_FakeQuery":
return self
def first(self) -> object:
return self._result
class _FakeDB:
def __init__(self, result: object) -> None:
self._result = result
def query(self, *_args: object, **_kwargs: object) -> _FakeQuery:
return _FakeQuery(self._result)
@pytest.mark.asyncio
async def test_admin_system_check_update_returns_unavailable_payload(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import system as mod
monkeypatch.setattr(mod, "_get_current_version", lambda: "1.2.3")
result = mod._build_check_update_unavailable_response()
assert result == {
"current_version": "1.2.3",
"latest_version": None,
"has_update": False,
"release_url": None,
"release_notes": None,
"published_at": None,
"error": "检查更新需要 Rust 管理后端",
}
@pytest.mark.asyncio
async def test_admin_system_aws_regions_uses_local_cache_when_present(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import system as mod
from src.core.cache_service import CacheService
monkeypatch.setattr(CacheService, "get", AsyncMock(return_value=["us-east-1", "us-west-2"]))
mod._aws_regions_mem_cache = None
result = await mod._get_aws_regions_response()
assert result == {"regions": ["us-east-1", "us-west-2"]}
@pytest.mark.asyncio
async def test_admin_system_aws_regions_raises_without_local_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import system as mod
from src.core.cache_service import CacheService
monkeypatch.setattr(CacheService, "get", AsyncMock(return_value=None))
mod._aws_regions_mem_cache = None
with pytest.raises(HTTPException) as exc_info:
await mod._get_aws_regions_response()
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "AWS regions requires Rust admin backend"
@pytest.mark.asyncio
async def test_admin_gemini_file_upload_requires_rust_uploader() -> None:
from src.api.admin import gemini_files as mod
upload = UploadFile(filename="example.txt", file=BytesIO(b"hello"), headers=None)
with pytest.raises(HTTPException) as exc_info:
await mod._upload_file_response(file=upload, key_ids="key_1")
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin Gemini file upload requires Rust uploader"
@pytest.mark.asyncio
async def test_admin_external_models_returns_cached_data(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
monkeypatch.setattr(
mod,
"_get_cached_data",
AsyncMock(return_value={"openai": {"official": True, "models": []}}),
)
response = await mod._get_external_models_response()
assert isinstance(response, JSONResponse)
assert response.status_code == 200
assert b'"official":true' in response.body
@pytest.mark.asyncio
async def test_admin_external_models_raise_without_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
monkeypatch.setattr(mod, "_get_cached_data", AsyncMock(return_value=None))
with pytest.raises(HTTPException) as exc_info:
await mod._get_external_models_response()
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "External models catalog requires Rust admin backend"
@pytest.mark.asyncio
async def test_admin_video_proxy_raises_when_google_proxy_is_required(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.video_tasks import routes as mod
from src.utils import auth_utils
task = SimpleNamespace(
id="task_1",
user_id="user_1",
video_url="https://generativelanguage.googleapis.com/v1/media/video.mp4",
)
request = SimpleNamespace(cookies={}, headers={"Authorization": "Bearer token"})
db = _FakeDB(task)
monkeypatch.setattr(
auth_utils,
"authenticate_user_from_bearer_token",
AsyncMock(return_value=SimpleNamespace(id="admin_1", role=mod.UserRole.ADMIN)),
)
with pytest.raises(HTTPException) as exc_info:
await mod._proxy_video_stream_response(task_id="task_1", request=request, token=None, db=db)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin video proxy requires Rust/public download path"
@pytest.mark.asyncio
async def test_admin_usage_replay_requires_rust_maintenance_backend() -> None:
from src.api.admin.usage import routes as mod
adapter = mod.AdminUsageReplayAdapter(usage_id="usage_1")
with pytest.raises(HTTPException) as exc_info:
await adapter.handle(SimpleNamespace())
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin usage replay requires Rust maintenance backend"
@pytest.mark.asyncio
@pytest.mark.parametrize(
("call_factory"),
[
lambda mod: mod.start_oauth("key_1", request=SimpleNamespace(), db=None, _=None),
lambda mod: mod.complete_oauth(
"key_1",
mod.CompleteOAuthRequest(callback_url="http://localhost/?code=x&state=y"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.refresh_oauth("key_1", request=SimpleNamespace(), db=None, _=None),
lambda mod: mod.start_provider_oauth(
"provider_1", request=SimpleNamespace(), db=None, _=None
),
lambda mod: mod.complete_provider_oauth(
"provider_1",
mod.ProviderCompleteOAuthRequest(
callback_url="http://localhost/?code=x&state=y",
),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.import_refresh_token(
"provider_1",
mod.ImportRefreshTokenRequest(refresh_token="refresh-token"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.batch_import_oauth(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.start_batch_import_oauth_task(
"provider_1",
mod.BatchImportRequest(credentials="refresh-token"),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.get_batch_import_oauth_task_status(
"provider_1",
"task_1",
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.device_authorize(
"provider_1",
mod.DeviceAuthorizeRequest(),
request=SimpleNamespace(),
db=None,
_=None,
),
lambda mod: mod.device_poll(
"provider_1",
mod.DevicePollRequest(session_id="session_1"),
request=SimpleNamespace(),
db=None,
_=None,
),
],
)
async def test_admin_provider_oauth_routes_require_rust_maintenance_backend(
call_factory,
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import provider_oauth as mod
async def _fake_pipeline_run(*, adapter: object, http_request: object, db: object, mode: object):
_ = http_request, db, mode
context = SimpleNamespace(add_audit_metadata=lambda **_kwargs: None)
return await adapter.handle(context) # type: ignore[attr-defined]
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
with pytest.raises(HTTPException) as exc_info:
await call_factory(mod)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "Admin provider OAuth requires Rust maintenance backend"

View File

@@ -0,0 +1,257 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from starlette.requests import Request
def _make_request(path: str, method: str = "POST") -> Request:
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": method,
"scheme": "http",
"path": path,
"raw_path": path.encode(),
"query_string": b"",
"headers": [],
"client": ("127.0.0.1", 12345),
"server": ("testserver", 80),
}
return Request(scope)
@pytest.mark.asyncio
async def test_admin_external_models_route_uses_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request("/api/admin/models/external", method="GET")
result = await mod.get_external_models(request=request, db=db, _=SimpleNamespace())
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.AdminGetExternalModelsAdapter)
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
@pytest.mark.parametrize(
("path", "adapter_type"),
[
("/api/admin/system/version", "AdminSystemVersionAdapter"),
("/api/admin/system/check-update", "AdminSystemCheckUpdateAdapter"),
("/api/admin/system/aws-regions", "AdminAwsRegionsAdapter"),
],
)
async def test_admin_system_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
path: str,
adapter_type: str,
) -> None:
from src.api.admin import system as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request(path, method="GET")
if path.endswith("/version"):
result = await mod.get_system_version(request=request, db=db)
elif path.endswith("/check-update"):
result = await mod.check_update(request=request, db=db)
else:
result = await mod.get_aws_regions(request=request, db=db)
assert result == {"ok": True}
assert type(captured["adapter"]).__name__ == adapter_type
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
async def test_admin_video_proxy_route_uses_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.video_tasks import routes as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request("/api/admin/video-tasks/task_1/video", method="GET")
result = await mod.proxy_video_stream(task_id="task_1", request=request, token="query-token", db=db)
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.VideoTaskProxyVideoAdapter)
assert captured["adapter"].task_id == "task_1"
assert captured["adapter"].token == "query-token"
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
async def test_admin_external_models_cache_route_uses_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
captured: dict[str, object] = {}
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.update(
{"adapter": adapter, "request": http_request, "db": db, "mode": mode},
)
return {"cleared": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
request = _make_request("/api/admin/models/external/cache", method="DELETE")
result = await mod.clear_external_models_cache(request=request, db=db, _=SimpleNamespace())
assert result == {"cleared": True}
assert isinstance(captured["adapter"], mod.AdminClearExternalModelsCacheAdapter)
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
@pytest.mark.asyncio
async def test_admin_external_models_adapters_delegate_to_helpers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin.models import external as mod
get_cached = AsyncMock(return_value={"ok": True})
clear_cached = AsyncMock(return_value={"cleared": True})
monkeypatch.setattr(mod, "_get_external_models_response", get_cached)
monkeypatch.setattr(mod, "_clear_external_models_cache_response", clear_cached)
get_result = await mod.AdminGetExternalModelsAdapter().handle(SimpleNamespace())
clear_result = await mod.AdminClearExternalModelsCacheAdapter().handle(SimpleNamespace())
assert get_result == {"ok": True}
assert clear_result == {"cleared": True}
get_cached.assert_awaited_once()
clear_cached.assert_awaited_once()
@pytest.mark.asyncio
async def test_provider_query_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import provider_query as mod
from fastapi import HTTPException
captured: list[dict[str, object]] = []
async def fake_run(*, adapter, http_request, db, mode, **_kwargs):
captured.append({"adapter": adapter, "request": http_request, "db": db, "mode": mode})
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = SimpleNamespace(name="db")
models_payload = mod.ModelsQueryRequest(provider_id="provider_1")
models_request = _make_request("/api/admin/provider-query/models")
with pytest.raises(HTTPException) as models_exc:
await mod.query_available_models(models_payload, models_request, db=db)
test_payload = mod.TestModelRequest(provider_id="provider_1", model_name="gpt-4o")
test_request = _make_request("/api/admin/provider-query/test-model")
with pytest.raises(HTTPException) as test_exc:
await mod.test_model(test_payload, test_request, db=db)
failover_payload = mod.TestModelFailoverRequest(
provider_id="provider_1",
mode="direct",
model_name="gpt-4o",
)
failover_request = _make_request("/api/admin/provider-query/test-model-failover")
with pytest.raises(HTTPException) as failover_exc:
await mod.test_model_failover(failover_payload, failover_request, db=db)
assert models_exc.value.status_code == 503
assert test_exc.value.status_code == 503
assert failover_exc.value.status_code == 503
assert "requires Rust maintenance backend" in str(models_exc.value.detail)
assert captured == []
@pytest.mark.asyncio
async def test_provider_query_adapters_delegate_to_helpers(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.admin import provider_query as mod
models_helper = AsyncMock(return_value={"kind": "models"})
test_helper = AsyncMock(return_value={"kind": "test"})
failover_helper = AsyncMock(return_value={"kind": "failover"})
monkeypatch.setattr(mod, "_query_available_models_response", models_helper)
monkeypatch.setattr(mod, "_test_model_response", test_helper)
monkeypatch.setattr(mod, "_test_model_failover_response", failover_helper)
db = SimpleNamespace(name="db")
user = SimpleNamespace(id="user_1")
request = _make_request("/api/admin/provider-query/test-model-failover")
context = SimpleNamespace(db=db, user=user, request=request)
models_payload = mod.ModelsQueryRequest(provider_id="provider_1")
test_payload = mod.TestModelRequest(provider_id="provider_1", model_name="gpt-4o")
failover_payload = mod.TestModelFailoverRequest(
provider_id="provider_1",
mode="direct",
model_name="gpt-4o",
)
models_result = await mod.ProviderQueryModelsAdapter(payload=models_payload).handle(context)
test_result = await mod.ProviderQueryTestModelAdapter(payload=test_payload).handle(context)
failover_result = await mod.ProviderQueryTestModelFailoverAdapter(
payload=failover_payload
).handle(context)
assert models_result == {"kind": "models"}
assert test_result == {"kind": "test"}
assert failover_result == {"kind": "failover"}
models_helper.assert_awaited_once_with(models_payload, db)
test_helper.assert_awaited_once_with(test_payload, db, user)
failover_helper.assert_awaited_once_with(failover_payload, request, db, user)

View File

@@ -8,6 +8,7 @@ import pytest
import src.api.handlers.base.chat_sync_executor as chat_sync_mod
from src.api.handlers.base.chat_sync_executor import ChatSyncExecutor
from src.core.exceptions import EmbeddedErrorException
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.executor_plan import (
ExecutionPlan,
ExecutionPlanBody,
@@ -145,15 +146,11 @@ async def test_execute_sync_plan_uses_rust_executor_when_available(
headers={"content-type": "application/json"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
@@ -184,15 +181,11 @@ async def test_execute_sync_plan_allows_supported_proxy_urls_for_rust(
headers={"content-type": "application/json"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
@@ -222,15 +215,11 @@ async def test_execute_sync_plan_allows_tunnel_delegate_for_rust(
headers={"content-type": "application/json"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
@@ -258,15 +247,11 @@ async def test_execute_sync_plan_allows_tls_profile_for_rust(
headers={"content-type": "application/json"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
@@ -294,15 +279,11 @@ async def test_execute_sync_plan_applies_envelope_postprocessing_after_rust(
headers={"content-type": "application/json"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
@@ -357,16 +338,12 @@ async def test_execute_sync_plan_applies_format_conversion_after_rust(
headers={"content-type": "application/json"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(chat_sync_mod, "get_format_converter_registry", lambda: _FakeRegistry())
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
@@ -420,9 +397,6 @@ async def test_execute_sync_plan_aggregates_upstream_stream_after_rust(
headers={"content-type": "text/event-stream"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
@@ -433,7 +407,6 @@ async def test_execute_sync_plan_aggregates_upstream_stream_after_rust(
"src.api.handlers.base.upstream_stream_bridge.aggregate_upstream_stream_to_internal_response",
_fake_aggregate,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
@@ -467,15 +440,11 @@ async def test_execute_sync_plan_turns_rust_http_error_into_httpx_status_error(
headers={"content-type": "application/json"},
)
async def _should_not_fallback(**kwargs: object) -> dict[str, object]:
raise AssertionError("local execution should not be used")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _should_not_fallback)
with pytest.raises(httpx.HTTPStatusError) as exc_info:
await executor._execute_sync_plan(
@@ -529,7 +498,7 @@ async def test_execute_sync_plan_preserves_embedded_error_semantics_from_rust(
@pytest.mark.asyncio
async def test_execute_sync_plan_falls_back_to_local_when_rust_unavailable(
async def test_execute_sync_plan_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
@@ -541,25 +510,57 @@ async def test_execute_sync_plan_falls_back_to_local_when_rust_unavailable(
assert plan.request_id == "req-test"
raise RustExecutorClientError("executor down")
fallback_called = False
async def _fake_local_execute(**kwargs: object) -> dict[str, object]:
nonlocal fallback_called
fallback_called = True
return {"id": "local-fallback"}
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(executor, "_execute_sync_plan_locally", _fake_local_execute)
response = await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
with pytest.raises(ProviderNotAvailableException) as exc_info:
await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
async def test_execute_sync_plan_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
) -> None:
executor = _make_executor()
prepared_plan = _make_prepared_plan()
prepared_plan.contract.proxy = chat_sync_mod.ExecutionProxySnapshot(
enabled=True,
mode="tunnel",
label="relay-node",
)
assert fallback_called is True
assert response == {"id": "local-fallback"}
monkeypatch.setattr(chat_sync_mod.config, "executor_backend", "rust")
async def _should_not_call_rust(self: object, plan: ExecutionPlan) -> RustExecutorSyncResult:
del self, plan
raise AssertionError("rust executor should not be called")
monkeypatch.setattr(
chat_sync_mod.RustExecutorClient,
"execute_sync_json",
_should_not_call_rust,
)
assert prepared_plan.remote_eligible is False
with pytest.raises(ProviderNotAvailableException) as exc_info:
await executor._execute_sync_plan(
prepared_plan=prepared_plan,
provider=SimpleNamespace(name="provider"),
model="gpt-4.1",
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == (
"execution contract is not eligible for rust executor"
)

View File

@@ -11,6 +11,7 @@ import src.api.handlers.base.chat_handler_base as chatmod
import src.services.proxy_node.resolver as proxymod
from src.api.handlers.base.chat_handler_base import ChatHandlerBase
from src.api.handlers.base.stream_context import StreamContext
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.rust_executor_client import (
RustExecutorClientError,
RustExecutorStreamResult,
@@ -78,6 +79,52 @@ class _FakeStreamProcessor:
await response_ctx.__aexit__(None, None, None)
class _FakeParser:
def is_error_response(self, response_json: dict[str, Any]) -> bool:
del response_json
return False
class _FakeInternalUsage:
input_tokens = 3
output_tokens = 5
cache_read_tokens = 1
cache_write_tokens = 0
class _FakeInternalResponse:
def __init__(self) -> None:
self.id = "resp-sync"
self.model = ""
self.usage = _FakeInternalUsage()
class _FakeSourceNormalizer:
def response_to_internal(self, response_json: dict[str, Any]) -> _FakeInternalResponse:
assert response_json == {"id": "sync-1", "message": "hello"}
return _FakeInternalResponse()
class _FakeTargetNormalizer:
def stream_event_from_internal(
self,
event: dict[str, Any],
state: Any,
) -> list[dict[str, Any]]:
assert event == {"kind": "chunk"}
assert getattr(state, "message_id", "") == "resp-sync"
return [{"delta": "hello"}]
class _FakeRegistry:
def get_normalizer(self, format_id: str) -> Any:
if format_id == "provider:test":
return _FakeSourceNormalizer()
if format_id == "openai:chat":
return _FakeTargetNormalizer()
raise AssertionError(f"unexpected format: {format_id}")
class _DummyChatHandler(ChatHandlerBase):
FORMAT_ID = "openai:chat"
@@ -200,7 +247,13 @@ async def test_execute_stream_request_uses_rust_executor_when_available(
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
@@ -251,6 +304,110 @@ async def test_execute_stream_request_uses_rust_executor_when_available(
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_execute_stream_request_uses_rust_sync_executor_for_non_stream_upstream(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
stream_processor = _FakeStreamProcessor()
stream_processor.on_streaming_start = None
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
async def _fake_prepare_provider_request(self: object, **kwargs: Any) -> object:
del self, kwargs
return chatmod.ProviderRequestResult(
request_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
url_model="gpt-test",
mapped_model=None,
envelope=None,
extra_headers={},
upstream_is_stream=False,
needs_conversion=False,
provider_api_format="provider:test",
client_api_format="openai:chat",
auth_info=_DummyAuthInfo(),
tls_profile=None,
)
async def _fake_execute_sync_json(self: object, plan: object) -> object:
del self
assert getattr(plan, "stream") is False
return SimpleNamespace(
status_code=200,
response_json={"id": "sync-1", "message": "hello"},
response_body_bytes=None,
headers={"content-type": "application/json"},
)
async def _should_not_call_stream(self: object, plan: object) -> RustExecutorStreamResult:
del self, plan
raise AssertionError("stream executor should not be used")
async def _should_not_get_http_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python upstream client should not be used")
monkeypatch.setattr(
_DummyChatHandler,
"_prepare_provider_request",
_fake_prepare_provider_request,
)
monkeypatch.setattr(chatmod, "get_format_converter_registry", lambda: _FakeRegistry())
monkeypatch.setattr(
chatmod,
"iter_internal_response_as_stream_events",
lambda internal_resp: [{"kind": "chunk"}],
)
monkeypatch.setattr(chatmod, "get_parser_for_format", lambda _format: _FakeParser())
monkeypatch.setattr(chatmod.RustExecutorClient, "execute_sync_json", _fake_execute_sync_json)
monkeypatch.setattr(chatmod.RustExecutorClient, "execute_stream", _should_not_call_stream)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_should_not_get_http_client,
)
stream = await handler._execute_stream_request(
ctx,
stream_processor,
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
received = [chunk async for chunk in stream]
assert received == [
b'data: {"delta": "hello"}\n\n',
b"data: [DONE]\n\n",
]
assert ctx.status_code == 200
assert ctx.input_tokens == 3
assert ctx.output_tokens == 5
assert ctx.cached_tokens == 1
@pytest.mark.asyncio
async def test_execute_stream_request_accepts_async_generator_stream_processor(
monkeypatch: pytest.MonkeyPatch,
@@ -501,7 +658,7 @@ async def test_execute_stream_request_turns_rust_upstream_error_into_http_status
@pytest.mark.asyncio
async def test_execute_stream_request_falls_back_to_python_when_rust_unavailable(
async def test_execute_stream_request_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
@@ -525,12 +682,8 @@ async def test_execute_stream_request_falls_back_to_python_when_rust_unavailable
del plan
raise RustExecutorClientError("executor down")
class _FakeHTTPClient:
def stream(self, **kwargs: Any) -> Any:
raise RuntimeError("local-http-client-used")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> _FakeHTTPClient:
return _FakeHTTPClient()
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(chatmod.RustExecutorClient, "execute_stream", _fake_execute_stream)
monkeypatch.setattr(
@@ -538,7 +691,7 @@ async def test_execute_stream_request_falls_back_to_python_when_rust_unavailable
_fake_get_upstream_client,
)
with pytest.raises(RuntimeError) as exc_info:
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler._execute_stream_request(
ctx,
object(),
@@ -550,4 +703,58 @@ async def test_execute_stream_request_falls_back_to_python_when_rust_unavailable
candidate=candidate,
)
assert "local-http-client-used" in str(exc_info.value)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
async def test_execute_stream_request_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_stream_setup(monkeypatch)
monkeypatch.setattr(chatmod.config, "executor_backend", "rust")
handler = _DummyChatHandler()
ctx = StreamContext(model="gpt-test", api_format="openai:chat")
ctx.client_api_format = "openai:chat"
provider = SimpleNamespace(name="provider", id="provider-1", provider_type="", proxy=None)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:chat", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None)
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
async def _should_not_call_rust(self: object, plan: object) -> RustExecutorStreamResult:
del self, plan
raise AssertionError("rust executor should not be called")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(chatmod, "is_remote_contract_eligible", lambda plan: False)
monkeypatch.setattr(chatmod.RustExecutorClient, "execute_stream", _should_not_call_rust)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler._execute_stream_request(
ctx,
object(),
provider,
endpoint,
key,
{"model": "gpt-test", "messages": [{"role": "user", "content": "hello"}]},
{},
candidate=candidate,
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == (
"execution contract is not eligible for rust executor"
)

View File

@@ -14,7 +14,9 @@ import src.services.task as taskmod
from src.api.handlers.base.cli_stream_mixin import CliStreamMixin
from src.api.handlers.base.cli_sync_mixin import CliSyncMixin
from src.api.handlers.base.stream_context import StreamContext
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.rust_executor_client import (
RustExecutorClientError,
RustExecutorStreamResult,
RustExecutorSyncResult,
)
@@ -46,7 +48,7 @@ class _DummyTelemetry:
class _DummySyncHandler(CliSyncMixin):
FORMAT_ID = "openai:cli"
def __init__(self) -> None:
def __init__(self, *, upstream_is_stream: bool = False) -> None:
self.db = None
self.redis = None
self.user = SimpleNamespace(id="user-1")
@@ -62,6 +64,7 @@ class _DummySyncHandler(CliSyncMixin):
self.telemetry = _DummyTelemetry()
self.perf_metrics = None
self._parser = _DummyParser()
self._upstream_is_stream = upstream_is_stream
@property
def parser(self) -> _DummyParser:
@@ -120,7 +123,7 @@ class _DummySyncHandler(CliSyncMixin):
url="https://upstream.test/v1/responses",
url_model=str(payload.get("model") or ""),
envelope=None,
upstream_is_stream=False,
upstream_is_stream=self._upstream_is_stream,
tls_profile=None,
selected_base_url=None,
)
@@ -299,6 +302,243 @@ async def test_cli_process_sync_uses_rust_executor_when_available(
assert json.loads(response.body) == {"id": "resp-rust-cli"}
@pytest.mark.asyncio
async def test_cli_process_sync_aggregates_upstream_stream_after_rust(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummySyncHandler(upstream_is_stream=True)
monkeypatch.setattr(cli_sync_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
class _FakeTaskService:
def __init__(self, db: Any, redis: Any) -> None:
del db, redis
async def execute(self, **kwargs: Any) -> Any:
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli")
key = SimpleNamespace(id="key-1", api_key="sk-test", proxy=None)
response = await kwargs["request_func"](provider, endpoint, key, candidate)
return SimpleNamespace(
response=response,
provider_name="provider",
request_candidate_id="cand-1",
provider_id="provider-1",
endpoint_id="endpoint-1",
key_id="key-1",
pool_summary=None,
)
class _FakeNormalizer:
def response_from_internal(self, response: Any, *, requested_model: str) -> dict[str, Any]:
return {
"aggregated": True,
"requested_model": requested_model,
"internal_id": response.id,
}
class _FakeRegistry:
def get_normalizer(self, format_id: str) -> _FakeNormalizer:
assert format_id == "openai:cli"
return _FakeNormalizer()
captured_chunks: list[bytes] = []
async def _fake_aggregate(
byte_iter: object,
*,
provider_api_format: str,
provider_name: str,
model: str,
request_id: str,
envelope: object = None,
provider_parser: object = None,
) -> object:
del envelope, provider_parser
async for chunk in byte_iter: # type: ignore[attr-defined]
captured_chunks.append(chunk)
assert provider_api_format == "openai:cli"
assert provider_name == "provider"
assert model == "gpt-4.1"
assert request_id == "req-cli-sync"
return SimpleNamespace(id="agg-cli-1")
async def _fake_execute_sync_json(self: object, plan: object) -> RustExecutorSyncResult:
assert getattr(plan, "provider_api_format") == "openai:cli"
assert getattr(plan, "stream") is True
return RustExecutorSyncResult(
status_code=200,
response_body_bytes=b"data: {\"id\":\"chunk-1\"}\n\ndata: [DONE]\n\n",
headers={"content-type": "text/event-stream"},
)
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(taskmod, "TaskService", _FakeTaskService)
monkeypatch.setattr(
cli_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(cli_sync_mod, "get_format_converter_registry", lambda: _FakeRegistry())
monkeypatch.setattr(
cli_sync_mod,
"aggregate_upstream_stream_to_internal_response",
_fake_aggregate,
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
response = await handler.process_sync(
original_request_body={"model": "gpt-4.1", "input": "hello"},
original_headers={},
)
assert response.status_code == 200
assert json.loads(response.body) == {
"aggregated": True,
"requested_model": "gpt-4.1",
"internal_id": "agg-cli-1",
}
assert captured_chunks == [b"data: {\"id\":\"chunk-1\"}\n\ndata: [DONE]\n\n"]
@pytest.mark.asyncio
async def test_cli_process_sync_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummySyncHandler()
monkeypatch.setattr(cli_sync_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
class _FakeTaskService:
def __init__(self, db: Any, redis: Any) -> None:
del db, redis
async def execute(self, **kwargs: Any) -> Any:
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli")
key = SimpleNamespace(id="key-1", api_key="sk-test", proxy=None)
await kwargs["request_func"](provider, endpoint, key, candidate)
raise AssertionError("task service should not reach Python local execution")
async def _fake_execute_sync_json(self: object, plan: object) -> RustExecutorSyncResult:
del self, plan
raise RustExecutorClientError("executor down")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(taskmod, "TaskService", _FakeTaskService)
monkeypatch.setattr(
cli_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler.process_sync(
original_request_body={"model": "gpt-4.1", "input": "hello"},
original_headers={},
)
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
async def test_cli_process_sync_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
) -> None:
handler = _DummySyncHandler()
monkeypatch.setattr(cli_sync_mod.config, "executor_backend", "rust")
monkeypatch.setattr(cli_sync_mod, "is_remote_contract_eligible", lambda plan: False)
_patch_proxy_resolver(monkeypatch)
class _FakeTaskService:
def __init__(self, db: Any, redis: Any) -> None:
del db, redis
async def execute(self, **kwargs: Any) -> Any:
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli")
key = SimpleNamespace(id="key-1", api_key="sk-test", proxy=None)
await kwargs["request_func"](provider, endpoint, key, candidate)
raise AssertionError("task service should not complete after local upstream attempt")
async def _fake_execute_sync_json(self: object, plan: object) -> RustExecutorSyncResult:
raise AssertionError("rust executor should not be used when contract is ineligible")
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(taskmod, "TaskService", _FakeTaskService)
monkeypatch.setattr(
cli_sync_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
await handler.process_sync(
original_request_body={"model": "gpt-4.1", "input": "hello"},
original_headers={},
)
assert exc_info.value.message == "CLI 请求暂不支持当前 Rust executor 契约"
assert exc_info.value.upstream_response == "remote_contract_ineligible"
@pytest.mark.asyncio
async def test_cli_execute_stream_request_uses_rust_sync_bridge(
monkeypatch: pytest.MonkeyPatch,
@@ -355,11 +595,163 @@ async def test_cli_execute_stream_request_uses_rust_sync_bridge(
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
chunks = [chunk async for chunk in stream]
assert chunks == [b"data: cli-bridge\n\n"]
@pytest.mark.asyncio
@pytest.mark.parametrize("upstream_is_stream", [False, True])
async def test_cli_execute_stream_request_raises_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
upstream_is_stream: bool,
) -> None:
handler = _DummyCliStreamHandler(upstream_is_stream=upstream_is_stream)
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
monkeypatch.setattr(cli_stream_mod.config, "executor_backend", "rust")
_patch_proxy_resolver(monkeypatch)
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None, api_key="sk-test", auth_type="")
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
if upstream_is_stream:
async def _fake_execute_stream(self: object, plan: object) -> RustExecutorStreamResult:
del self, plan
raise RustExecutorClientError("executor down")
monkeypatch.setattr(
cli_stream_mod.RustExecutorClient,
"execute_stream",
_fake_execute_stream,
)
else:
async def _fake_execute_sync_json(self: object, plan: object) -> RustExecutorSyncResult:
del self, plan
raise RustExecutorClientError("executor down")
monkeypatch.setattr(
cli_stream_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
stream = await handler._execute_stream_request(
ctx,
provider,
endpoint,
key,
{"model": "gpt-test", "input": "hello"},
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
_ = [chunk async for chunk in stream]
assert exc_info.value.message == "执行器暂时不可用,请稍后重试"
assert exc_info.value.upstream_response == "executor down"
@pytest.mark.asyncio
@pytest.mark.parametrize("upstream_is_stream", [False, True])
async def test_cli_execute_stream_request_raises_when_remote_contract_is_ineligible(
monkeypatch: pytest.MonkeyPatch,
upstream_is_stream: bool,
) -> None:
handler = _DummyCliStreamHandler(upstream_is_stream=upstream_is_stream)
ctx = StreamContext(model="gpt-test", api_format="openai:cli")
ctx.client_api_format = "openai:cli"
monkeypatch.setattr(cli_stream_mod.config, "executor_backend", "rust")
monkeypatch.setattr(cli_stream_mod, "is_remote_contract_eligible", lambda plan: False)
_patch_proxy_resolver(monkeypatch)
async def _fake_get_upstream_client(*args: Any, **kwargs: Any) -> object:
raise AssertionError("python fallback should not be used")
async def _fake_execute_sync_json(self: object, plan: object) -> RustExecutorSyncResult:
raise AssertionError("rust sync executor should not be used when contract is ineligible")
async def _fake_execute_stream(self: object, plan: object) -> RustExecutorStreamResult:
raise AssertionError("rust stream executor should not be used when contract is ineligible")
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_upstream_client",
_fake_get_upstream_client,
)
monkeypatch.setattr(
cli_stream_mod.RustExecutorClient,
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(
cli_stream_mod.RustExecutorClient,
"execute_stream",
_fake_execute_stream,
)
provider = SimpleNamespace(
name="provider",
id="provider-1",
provider_type="",
proxy=None,
request_timeout=None,
stream_first_byte_timeout=None,
)
endpoint = SimpleNamespace(id="endpoint-1", api_format="openai:cli", base_url="https://x")
key = SimpleNamespace(id="key-1", proxy=None, api_key="sk-test", auth_type="")
candidate = SimpleNamespace(
request_candidate_id="cand-1",
mapping_matched_model=None,
needs_conversion=False,
output_limit=None,
)
with pytest.raises(ProviderNotAvailableException) as exc_info:
stream = await handler._execute_stream_request(
ctx,
provider,
endpoint,
key,
{"model": "gpt-test", "input": "hello"},
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
_ = [chunk async for chunk in stream]
assert exc_info.value.message == "CLI 请求暂不支持当前 Rust executor 契约"
assert exc_info.value.upstream_response == "remote_contract_ineligible"
@pytest.mark.asyncio
async def test_cli_execute_stream_request_uses_rust_native_stream(
monkeypatch: pytest.MonkeyPatch,
@@ -417,6 +809,8 @@ async def test_cli_execute_stream_request_uses_rust_native_stream(
{},
candidate=candidate,
)
if hasattr(stream, "__await__"):
stream = await stream
chunks = [chunk async for chunk in stream]
assert chunks == [

View File

@@ -166,3 +166,27 @@ async def test_endpoint_checker_proxy_snapshot_falls_back_to_system_proxy(
assert snapshot.enabled is True
assert snapshot.url == "http://system-proxy.test:8080"
assert snapshot.mode == "http"
@pytest.mark.asyncio
async def test_endpoint_checker_returns_503_when_rust_executor_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.base import endpoint_checker as mod
monkeypatch.setattr(mod.config, "executor_backend", "python")
executor = HttpRequestExecutor(timeout=5.0)
result = await executor.execute(
EndpointCheckRequest(
url="https://upstream.test/v1/chat/completions",
headers={"authorization": "Bearer test"},
json_body={"model": "gpt-test", "messages": [{"role": "user", "content": "hi"}]},
api_format="openai:chat",
provider_name="openai",
model_name="gpt-test",
)
)
assert result.status_code == 503
assert result.error_message == "端点检查仅支持 Rust executor"

View File

@@ -14,6 +14,7 @@ import src.services.proxy_node.resolver as resolver_mod
import src.services.request.rust_executor_client as rust_client_mod
from src.api.handlers.gemini.video_handler import GeminiVeoHandler
from src.core.api_format.conversion.internal_video import VideoStatus
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.rust_executor_client import RustExecutorStreamResult
@@ -82,12 +83,6 @@ async def test_handle_create_task_uses_rust_sync_helper(
"x-goog-api-key": upstream_key
},
)
monkeypatch.setattr(
video_mod.HTTPClientPool,
"get_default_client_async",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
async def _fake_rust_sync(**kwargs: object) -> httpx.Response:
assert kwargs["method"] == "POST"
assert kwargs["provider_id"] == "prov-1"
@@ -207,12 +202,6 @@ async def test_handle_download_content_uses_rust_executor_with_proxy_snapshot(
)
monkeypatch.setattr(rust_client_mod.RustExecutorClient, "execute_stream", _fake_execute_stream)
monkeypatch.setattr(
video_mod.HTTPClientPool,
"get_default_client_async",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
response = await handler.handle_download_content(
task_id="operations/ext-1",
http_request=SimpleNamespace(),
@@ -224,3 +213,39 @@ async def test_handle_download_content_uses_rust_executor_with_proxy_snapshot(
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"gemini-video"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_handle_download_content_raises_when_rust_backend_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "python")
handler = _make_handler()
monkeypatch.setattr(
handler,
"_get_task_by_external_id",
lambda task_id: SimpleNamespace(
id=task_id,
status=VideoStatus.COMPLETED.value,
video_url="https://storage.example.com/video.mp4",
video_expires_at=datetime.now(timezone.utc).replace(year=2099),
model="veo-3",
),
)
monkeypatch.setattr(
handler,
"_get_endpoint_and_key",
lambda task: (
SimpleNamespace(id="ep-1", provider_id="prov-1", proxy=None),
SimpleNamespace(id="key-1", api_key=None, proxy=None),
),
)
with pytest.raises(ProviderNotAvailableException):
await handler.handle_download_content(
task_id="operations/ext-1",
http_request=SimpleNamespace(),
original_headers={},
query_params=None,
)

View File

@@ -5,11 +5,12 @@ from types import SimpleNamespace
from typing import Any
import pytest
from fastapi.responses import Response, StreamingResponse
from fastapi.responses import StreamingResponse
import src.api.handlers.openai.video_handler as video_mod
from src.api.handlers.openai.video_handler import OpenAIVideoHandler
from src.core.api_format.conversion.internal_video import VideoStatus
from src.core.exceptions import ProviderNotAvailableException
from src.services.request.rust_executor_client import (
RustExecutorClientError,
RustExecutorStreamResult,
@@ -157,7 +158,7 @@ async def test_handle_download_content_uses_rust_executor_for_upstream_content_e
@pytest.mark.asyncio
async def test_handle_download_content_falls_back_to_python_proxy_when_rust_unavailable(
async def test_handle_download_content_raises_when_rust_executor_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(video_mod.config, "executor_backend", "rust")
@@ -178,21 +179,11 @@ async def test_handle_download_content_falls_back_to_python_proxy_when_rust_unav
del self, plan
raise RustExecutorClientError("executor down")
fallback_response = Response(content=b"python-fallback", media_type="video/mp4")
async def _fake_proxy_direct_url(url: str, task_id: str) -> Response:
assert url == "https://cdn.example.com/video.mp4"
assert task_id == "task-1"
return fallback_response
monkeypatch.setattr(video_mod.RustExecutorClient, "execute_stream", _failing_execute_stream)
monkeypatch.setattr(handler, "_proxy_direct_url", _fake_proxy_direct_url)
response = await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params={"variant": "video"},
)
assert response is fallback_response
with pytest.raises(ProviderNotAvailableException):
await handler.handle_download_content(
task_id="task-1",
http_request=SimpleNamespace(),
original_headers={},
query_params={"variant": "video"},
)

View File

@@ -2877,3 +2877,286 @@ async def test_build_openai_cli_stream_plan_rejects_force_rewrite_envelope(
)
assert result is None
@pytest.mark.asyncio
async def test_build_openai_cli_stream_plan_rejects_codex_transport_for_direct_executor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai_cli import OpenAICliAdapter
adapter = OpenAICliAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-cli-stream-codex-transport-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
extra={"perf": None},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "input": "hello", "stream": True}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-cli-stream-codex-transport-123",
name="codex",
proxy=None,
),
endpoint=SimpleNamespace(id="endpoint-cli-stream-codex-transport-123", api_format="openai:cli"),
key=SimpleNamespace(id="key-cli-stream-codex-transport-123", proxy=None),
mapping_matched_model="gpt-5",
needs_conversion=False,
output_limit=None,
request_candidate_id="cand-cli-stream-codex-transport-123",
)
fake_upstream_request = SimpleNamespace(
url="https://chatgpt.com/backend-api/codex/responses",
headers={"content-type": "application/json", "accept": "text/event-stream"},
payload={"model": "gpt-5", "input": "hello", "stream": True},
upstream_is_stream=True,
envelope=None,
tls_profile=None,
)
class FakeCliHandler:
primary_api_format = "openai:cli"
def __init__(self, **kwargs: object) -> None:
pass
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "gpt-5"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _get_mapped_model(self, **kwargs: object) -> str:
return "gpt-5"
async def _build_upstream_request(self, **kwargs: object) -> SimpleNamespace:
return fake_upstream_request
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(
type(adapter),
"HANDLER_CLASS",
property(lambda self: FakeCliHandler),
)
monkeypatch.setattr(
adapter,
"_merge_path_params",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-cli-stream-codex-transport-123"),
SimpleNamespace(id="api-key-cli-stream-codex-transport-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route("POST", "/v1/responses")
auth_context = GatewayAuthContext(
user_id="user-cli-stream-codex-transport-123",
api_key_id="api-key-cli-stream-codex-transport-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/responses",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "input": "hello", "stream": True},
auth_context=auth_context,
)
result = await _build_openai_cli_stream_plan(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is None
@pytest.mark.asyncio
async def test_build_openai_cli_sync_plan_rejects_codex_transport_for_direct_executor(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.handlers.openai_cli import OpenAICliAdapter
adapter = OpenAICliAdapter()
fake_context = SimpleNamespace(
path_params={},
request_id="req-cli-sync-codex-transport-123",
client_ip="127.0.0.1",
user_agent="pytest",
start_time=0.0,
original_headers={"content-type": "application/json", "authorization": "Bearer test-key"},
query_params={},
client_content_encoding=None,
extra={"perf": None},
)
fake_context.ensure_json_body_async = AsyncMock(
return_value={"model": "gpt-5", "input": "hello"}
)
fake_candidate = SimpleNamespace(
provider=SimpleNamespace(
id="provider-cli-sync-codex-transport-123",
name="codex",
proxy=None,
request_timeout=None,
),
endpoint=SimpleNamespace(id="endpoint-cli-sync-codex-transport-123", api_format="openai:cli"),
key=SimpleNamespace(id="key-cli-sync-codex-transport-123", proxy=None),
mapping_matched_model="gpt-5",
needs_conversion=False,
output_limit=None,
request_candidate_id="cand-cli-sync-codex-transport-123",
)
fake_upstream_request = SimpleNamespace(
url="https://chatgpt.com/backendapi/codex/responses",
headers={"content-type": "application/json"},
payload={"model": "gpt-5", "input": "hello"},
upstream_is_stream=False,
envelope=None,
tls_profile=None,
)
class FakeCliHandler:
primary_api_format = "openai:cli"
def __init__(self, **kwargs: object) -> None:
pass
def extract_model_from_request(
self, body: dict[str, object], path_params: dict[str, str]
) -> str:
return "gpt-5"
def _resolve_capability_requirements(self, **kwargs: object) -> None:
return None
async def _resolve_preferred_key_ids(self, **kwargs: object) -> None:
return None
async def _get_mapped_model(self, **kwargs: object) -> str:
return "gpt-5"
async def _build_upstream_request(self, **kwargs: object) -> SimpleNamespace:
return fake_upstream_request
monkeypatch.setattr(adapter, "authorize", lambda context: None)
monkeypatch.setattr(
type(adapter),
"HANDLER_CLASS",
property(lambda self: FakeCliHandler),
)
monkeypatch.setattr(
adapter,
"_merge_path_params",
lambda body, path_params: body,
raising=False,
)
monkeypatch.setattr(
"src.api.internal.gateway._resolve_gateway_sync_adapter",
lambda decision, path: (adapter, {}),
)
monkeypatch.setattr(
"src.api.internal.gateway._load_gateway_auth_models",
lambda db, auth_context: (
SimpleNamespace(id="user-cli-sync-codex-transport-123"),
SimpleNamespace(id="api-key-cli-sync-codex-transport-123"),
),
)
monkeypatch.setattr(
"src.api.internal.gateway.get_pipeline",
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
monkeypatch.setattr(
"src.api.internal.gateway._build_gateway_request_context",
lambda **kwargs: fake_context,
)
monkeypatch.setattr(
"src.api.internal.gateway._select_gateway_direct_candidate",
AsyncMock(return_value=fake_candidate),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_proxy_info_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_delegate_config_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.build_proxy_url_async",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.get_system_proxy_config_async",
AsyncMock(return_value=None),
)
decision = classify_gateway_route("POST", "/v1/responses")
auth_context = GatewayAuthContext(
user_id="user-cli-sync-codex-transport-123",
api_key_id="api-key-cli-sync-codex-transport-123",
access_allowed=True,
)
payload = GatewayExecuteRequest(
method="POST",
path="/v1/responses",
headers={"content-type": "application/json", "authorization": "Bearer test-key"},
body_json={"model": "gpt-5", "input": "hello"},
auth_context=auth_context,
)
result = await _build_openai_cli_sync_plan(
request=SimpleNamespace(),
payload=payload,
db=object(),
auth_context=auth_context,
decision=decision,
)
assert result is None

View File

@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
assert predicate()
def _make_legacy_test_client(app: FastAPI) -> TestClient:
return TestClient(
app,
base_url="http://127.0.0.1",
headers={"x-aether-legacy-internal-gateway": "true"},
)
def test_finalize_sync_route_finalizes_openai_video_create_response(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -96,7 +104,7 @@ def test_finalize_sync_route_finalizes_openai_video_create_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -144,7 +152,7 @@ def test_finalize_sync_route_finalizes_openai_chat_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -193,7 +201,7 @@ def test_finalize_sync_route_finalizes_openai_cli_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -753,7 +761,7 @@ def test_finalize_sync_route_uses_chat_fast_path_without_db_session(
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -802,7 +810,7 @@ def test_finalize_sync_route_uses_cli_fast_path_without_db_session(
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -847,7 +855,7 @@ def test_finalize_sync_route_finalizes_openai_video_remix_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -896,7 +904,7 @@ def test_finalize_sync_route_finalizes_gemini_video_create_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -945,7 +953,7 @@ def test_finalize_sync_route_finalizes_openai_video_delete_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -984,7 +992,7 @@ def test_finalize_sync_route_finalizes_openai_video_cancel_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -1019,7 +1027,7 @@ def test_finalize_sync_route_finalizes_gemini_video_cancel_response(
finalize_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
@@ -1040,3 +1048,278 @@ def test_finalize_sync_route_finalizes_gemini_video_cancel_response(
assert response.headers[CONTROL_EXECUTED_HEADER] == "true"
assert response.json() == {}
finalize_mock.assert_awaited_once()
@pytest.mark.asyncio
async def test_finalize_gateway_openai_video_create_sync_reuses_rust_owned_task(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.internal import gateway as gateway_module
background_mock = AsyncMock(return_value=None)
begin_pending_usage = MagicMock()
class FakeQuery:
def __init__(self, value: Any) -> None:
self._value = value
def filter(self, *args: Any, **kwargs: Any) -> "FakeQuery":
return self
def first(self) -> Any:
return self._value
class FakeDB:
def __init__(self) -> None:
self._mapping = {
"User": SimpleNamespace(id="user-123"),
"ApiKey": SimpleNamespace(id="key-123"),
"VideoTask": SimpleNamespace(id="task-local-123"),
}
def query(self, model: Any) -> FakeQuery:
return FakeQuery(self._mapping.get(getattr(model, "__name__", "")))
def add(self, obj: Any) -> None:
raise AssertionError("legacy task creation should be skipped")
class FakeOpenAIVideoHandler:
FORMAT_ID = "openai:video"
def __init__(self, **kwargs: Any) -> None:
self._normalizer = SimpleNamespace(
video_task_from_internal=lambda task: {
"id": task["id"],
"object": "video",
"status": "submitted",
}
)
def _task_to_internal(self, task: Any) -> dict[str, Any]:
return {"id": task.id}
monkeypatch.setattr(
"src.api.handlers.openai.video_handler.OpenAIVideoHandler",
FakeOpenAIVideoHandler,
)
monkeypatch.setattr(
"src.api.internal.gateway._run_gateway_video_finalize_submitted_background",
background_mock,
)
monkeypatch.setattr(
"src.services.usage.service.UsageService.begin_pending_usage",
begin_pending_usage,
)
payload = GatewaySyncReportRequest(
trace_id="trace-openai-video-rust-owner-123",
report_kind="openai_video_create_sync_success",
report_context={
"user_id": "user-123",
"api_key_id": "key-123",
"provider_id": "provider-123",
"endpoint_id": "endpoint-123",
"key_id": "provider-key-123",
"request_id": "req-openai-video-rust-owner-123",
"local_task_id": "task-local-123",
"provider_name": "openai",
"provider_api_format": "openai:video",
"rust_video_task_persisted": True,
},
status_code=200,
headers={"content-type": "application/json"},
body_json={"id": "ext-video-123", "status": "submitted"},
telemetry={"elapsed_ms": 42},
)
response = await gateway_module._finalize_gateway_openai_video_create_sync(
payload,
db=FakeDB(),
)
assert response.status_code == 200
assert json.loads(response.body) == {
"id": "task-local-123",
"object": "video",
"status": "submitted",
}
begin_pending_usage.assert_not_called()
background_mock.assert_awaited_once()
@pytest.mark.asyncio
async def test_finalize_gateway_openai_video_remix_sync_reuses_rust_owned_task(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.internal import gateway as gateway_module
class FakeQuery:
def __init__(self, value: Any) -> None:
self._value = value
def filter(self, *args: Any, **kwargs: Any) -> "FakeQuery":
return self
def first(self) -> Any:
return self._value
class FakeDB:
def __init__(self) -> None:
self._mapping = {
"User": SimpleNamespace(id="user-123"),
"ApiKey": SimpleNamespace(id="key-123"),
"VideoTask": SimpleNamespace(id="task-local-remix-123"),
}
def query(self, model: Any) -> FakeQuery:
return FakeQuery(self._mapping.get(getattr(model, "__name__", "")))
def add(self, obj: Any) -> None:
raise AssertionError("legacy remix task creation should be skipped")
class FakeOpenAIVideoHandler:
def __init__(self, **kwargs: Any) -> None:
self._normalizer = SimpleNamespace(
video_task_from_internal=lambda task: {
"id": task["id"],
"object": "video",
"status": "submitted",
}
)
def _task_to_internal(self, task: Any) -> dict[str, Any]:
return {"id": task.id}
def _get_task(self, task_id: str) -> Any:
raise AssertionError("legacy remix source lookup should be skipped")
monkeypatch.setattr(
"src.api.handlers.openai.video_handler.OpenAIVideoHandler",
FakeOpenAIVideoHandler,
)
payload = GatewaySyncReportRequest(
trace_id="trace-openai-video-remix-rust-owner-123",
report_kind="openai_video_remix_sync_success",
report_context={
"user_id": "user-123",
"api_key_id": "key-123",
"task_id": "task-source-123",
"request_id": "req-openai-video-remix-rust-owner-123",
"local_task_id": "task-local-remix-123",
"rust_video_task_persisted": True,
},
status_code=200,
headers={"content-type": "application/json"},
body_json={"id": "ext-remix-task-123", "status": "submitted"},
)
response = await gateway_module._finalize_gateway_openai_video_remix_sync(
payload,
db=FakeDB(),
)
assert response.status_code == 200
assert json.loads(response.body) == {
"id": "task-local-remix-123",
"object": "video",
"status": "submitted",
}
@pytest.mark.asyncio
async def test_finalize_gateway_gemini_video_create_sync_reuses_rust_owned_task(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.internal import gateway as gateway_module
background_mock = AsyncMock(return_value=None)
begin_pending_usage = MagicMock()
class FakeQuery:
def __init__(self, value: Any) -> None:
self._value = value
def filter(self, *args: Any, **kwargs: Any) -> "FakeQuery":
return self
def first(self) -> Any:
return self._value
class FakeDB:
def __init__(self) -> None:
self._mapping = {
"User": SimpleNamespace(id="user-123"),
"ApiKey": SimpleNamespace(id="key-123"),
"VideoTask": SimpleNamespace(short_id="short12345678"),
}
def query(self, model: Any) -> FakeQuery:
return FakeQuery(self._mapping.get(getattr(model, "__name__", "")))
def add(self, obj: Any) -> None:
raise AssertionError("legacy gemini task creation should be skipped")
class FakeGeminiVeoHandler:
FORMAT_ID = "gemini:video"
def __init__(self, **kwargs: Any) -> None:
self._normalizer = SimpleNamespace(
video_task_from_internal=lambda task: {
"name": f"models/veo-3/operations/{task['id']}",
"done": False,
"metadata": {},
}
)
def _task_to_internal(self, task: Any) -> dict[str, Any]:
return {"id": task.short_id}
monkeypatch.setattr(
"src.api.handlers.gemini.video_handler.GeminiVeoHandler",
FakeGeminiVeoHandler,
)
monkeypatch.setattr(
"src.api.internal.gateway._run_gateway_video_finalize_submitted_background",
background_mock,
)
monkeypatch.setattr(
"src.services.usage.service.UsageService.begin_pending_usage",
begin_pending_usage,
)
payload = GatewaySyncReportRequest(
trace_id="trace-gemini-video-rust-owner-123",
report_kind="gemini_video_create_sync_success",
report_context={
"user_id": "user-123",
"api_key_id": "key-123",
"provider_id": "provider-123",
"endpoint_id": "endpoint-123",
"key_id": "provider-key-123",
"request_id": "req-gemini-video-rust-owner-123",
"model": "veo-3",
"local_short_id": "short12345678",
"provider_name": "gemini",
"provider_api_format": "gemini:video",
"rust_video_task_persisted": True,
},
status_code=200,
headers={"content-type": "application/json"},
body_json={"name": "operations/ext-video-123"},
telemetry={"elapsed_ms": 48},
)
response = await gateway_module._finalize_gateway_gemini_video_create_sync(
payload,
db=FakeDB(),
)
assert response.status_code == 200
assert json.loads(response.body) == {
"name": "models/veo-3/operations/short12345678",
"done": False,
"metadata": {},
}
begin_pending_usage.assert_not_called()
background_mock.assert_awaited_once()

View File

@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
assert predicate()
def _make_legacy_test_client(app: FastAPI) -> TestClient:
return TestClient(
app,
base_url="http://127.0.0.1",
headers={"x-aether-legacy-internal-gateway": "true"},
)
def test_report_sync_route_applies_gemini_files_mapping(monkeypatch: pytest.MonkeyPatch) -> None:
app = FastAPI()
app.include_router(router)
@@ -91,7 +99,7 @@ def test_report_sync_route_applies_gemini_files_mapping(monkeypatch: pytest.Monk
_fake_store_mapping,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -138,7 +146,7 @@ def test_report_sync_route_applies_gemini_files_delete_mapping(
_fake_delete_mapping,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -172,7 +180,7 @@ def test_report_sync_route_uses_lazy_session(
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -213,7 +221,7 @@ def test_report_sync_route_runs_video_create_success_inline(
lambda app_obj: ("db-inline-123", cleanup_mock),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={

View File

@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
assert predicate()
def _make_legacy_test_client(app: FastAPI) -> TestClient:
return TestClient(
app,
base_url="http://127.0.0.1",
headers={"x-aether-legacy-internal-gateway": "true"},
)
def test_report_stream_route_uses_lazy_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -91,7 +99,7 @@ def test_report_stream_route_uses_lazy_session(
lambda: (_ for _ in ()).throw(AssertionError("create_session should not be called")),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
@@ -170,7 +178,7 @@ def test_report_stream_route_records_openai_chat_stream_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
@@ -203,7 +211,7 @@ def test_report_stream_route_records_claude_chat_stream_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
@@ -240,7 +248,7 @@ def test_report_stream_route_records_gemini_chat_stream_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
@@ -277,7 +285,7 @@ def test_report_stream_route_records_openai_cli_stream_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
@@ -316,7 +324,7 @@ def test_report_stream_route_records_claude_cli_stream_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={
@@ -355,7 +363,7 @@ def test_report_stream_route_records_gemini_cli_stream_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-stream",
json={

View File

@@ -75,6 +75,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
assert predicate()
def _make_legacy_test_client(app: FastAPI) -> TestClient:
return TestClient(
app,
base_url="http://127.0.0.1",
headers={"x-aether-legacy-internal-gateway": "true"},
)
def test_report_sync_route_records_openai_chat_sync_success(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -88,7 +96,7 @@ def test_report_sync_route_records_openai_chat_sync_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -125,7 +133,7 @@ def test_report_sync_route_records_openai_cli_sync_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -166,7 +174,7 @@ def test_report_sync_route_records_claude_cli_sync_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -206,7 +214,7 @@ def test_report_sync_route_records_gemini_cli_sync_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -254,7 +262,7 @@ def test_report_sync_route_records_video_sync_success_variants(
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(f"src.api.internal.gateway.{recorder_attr}", record_mock)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -335,7 +343,7 @@ def test_report_sync_route_records_claude_chat_sync_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={
@@ -375,7 +383,7 @@ def test_report_sync_route_records_gemini_chat_sync_success(
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/report-sync",
json={

View File

@@ -273,3 +273,81 @@ def test_record_gateway_direct_candidate_graph_uses_selected_pool_key_index() ->
assert rows[0].status == "unused"
assert rows[1].status == "pending"
assert getattr(candidate, "request_candidate_id") == rows[1].id
def test_record_gateway_direct_candidate_graph_is_idempotent_for_same_request_id() -> None:
engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine, tables=[RequestCandidate.__table__])
SessionLocal = sessionmaker(bind=engine)
with SessionLocal() as db:
resolver = CandidateResolver(db=db, cache_scheduler=SimpleNamespace())
user_api_key = SimpleNamespace(
id="api-key-direct-idempotent-123",
name="client-key",
user_id="user-direct-idempotent-123",
user=SimpleNamespace(id="user-direct-idempotent-123", username="direct-user"),
)
request_id = "req-direct-candidate-idempotent-123"
first_candidates = [
ProviderCandidate(
provider=SimpleNamespace(id="provider-selected-123", name="openai", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-selected-123"),
key=SimpleNamespace(id="key-selected-123"),
provider_api_format="openai:cli",
),
ProviderCandidate(
provider=SimpleNamespace(id="provider-unused-123", name="openai", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-unused-123"),
key=SimpleNamespace(id="key-unused-123"),
provider_api_format="openai:cli",
),
]
_record_gateway_direct_candidate_graph(
db=db,
candidate_resolver=resolver,
candidates=first_candidates,
request_id=request_id,
user_api_key=user_api_key,
required_capabilities=None,
selected_candidate_index=0,
)
first_selected_record_id = getattr(first_candidates[0], "request_candidate_id")
second_candidates = [
ProviderCandidate(
provider=SimpleNamespace(id="provider-selected-123", name="openai", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-selected-123"),
key=SimpleNamespace(id="key-selected-123"),
provider_api_format="openai:cli",
),
ProviderCandidate(
provider=SimpleNamespace(id="provider-unused-123", name="openai", max_retries=1),
endpoint=SimpleNamespace(id="endpoint-unused-123"),
key=SimpleNamespace(id="key-unused-123"),
provider_api_format="openai:cli",
),
]
_record_gateway_direct_candidate_graph(
db=db,
candidate_resolver=resolver,
candidates=second_candidates,
request_id=request_id,
user_api_key=user_api_key,
required_capabilities=None,
selected_candidate_index=0,
)
rows = (
db.query(RequestCandidate)
.filter(RequestCandidate.request_id == request_id)
.order_by(RequestCandidate.candidate_index, RequestCandidate.retry_index)
.all()
)
assert len(rows) == 2
assert rows[0].status == "pending"
assert rows[1].status == "unused"
assert getattr(second_candidates[0], "request_candidate_id") == first_selected_record_id

View File

@@ -0,0 +1,21 @@
from __future__ import annotations
from pathlib import Path
def test_no_python_modules_outside_internal_gateway_reference_internal_gateway_urls() -> None:
repo_root = Path(__file__).resolve().parents[3]
scan_roots = [repo_root / "src" / "api", repo_root / "src" / "services"]
needle = "/api/internal/gateway"
offenders: list[str] = []
for root in scan_roots:
for path in root.rglob("*.py"):
rel = path.relative_to(repo_root).as_posix()
if rel.startswith("src/api/internal/"):
continue
text = path.read_text(encoding="utf-8")
if needle in text:
offenders.append(rel)
assert offenders == []

View File

@@ -75,6 +75,29 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
assert predicate()
def _make_legacy_test_client(app: FastAPI) -> TestClient:
return TestClient(
app,
base_url="http://127.0.0.1",
headers={"x-aether-legacy-internal-gateway": "true"},
)
def _make_internal_test_client(monkeypatch: pytest.MonkeyPatch) -> TestClient:
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
return TestClient(app, base_url="http://127.0.0.1")
def _assert_legacy_guard_response(response) -> None:
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
def test_build_gateway_sync_telemetry_writer_uses_queue_writer_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -314,7 +337,7 @@ def test_auth_context_route_returns_openai_bearer_auth_context(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/auth-context",
json={
@@ -371,14 +394,17 @@ def test_execute_sync_route_returns_controlled_response(monkeypatch: pytest.Monk
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"headers": {
"user-agent": "pytest",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
@@ -438,7 +464,7 @@ def test_execute_sync_route_resolves_auth_context_when_missing(
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
@@ -448,6 +474,7 @@ def test_execute_sync_route_resolves_auth_context_when_missing(
"headers": {
"user-agent": "pytest",
"authorization": "Bearer client-key",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": []},
},
@@ -465,14 +492,17 @@ def test_execute_sync_route_falls_back_for_stream_payload() -> None:
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-stream-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"headers": {
"user-agent": "pytest",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
"auth_context": {
"user_id": "user-123",
@@ -522,14 +552,17 @@ def test_execute_stream_route_returns_controlled_stream(monkeypatch: pytest.Monk
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-stream",
json={
"trace_id": "trace-stream-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"headers": {
"user-agent": "pytest",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
"auth_context": {
"user_id": "user-123",
@@ -567,7 +600,7 @@ def test_plan_sync_route_executes_control_when_direct_plan_missing(
execute_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -608,7 +641,7 @@ def test_plan_stream_route_executes_control_when_direct_plan_missing(
execute_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -626,13 +659,77 @@ def test_plan_stream_route_executes_control_when_direct_plan_missing(
execute_mock.assert_awaited_once()
def test_plan_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_internal_test_client(monkeypatch)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
"trace_id": "trace-guard-plan-sync",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"content-type": "application/json"},
"body_json": {"model": "gpt-5", "messages": []},
},
)
_assert_legacy_guard_response(response)
def test_plan_stream_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_internal_test_client(monkeypatch)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
"trace_id": "trace-guard-plan-stream",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"content-type": "application/json"},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
},
)
_assert_legacy_guard_response(response)
def test_report_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_internal_test_client(monkeypatch)
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-guard-report-sync",
"report_kind": "openai_chat_sync_finalize",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "application/json"},
},
)
_assert_legacy_guard_response(response)
def test_finalize_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_internal_test_client(monkeypatch)
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
"trace_id": "trace-guard-finalize-sync",
"report_kind": "openai_chat_sync_finalize",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "application/json"},
},
)
_assert_legacy_guard_response(response)
def test_execute_stream_route_falls_back_for_sync_payload(monkeypatch: pytest.MonkeyPatch) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-stream",
json={
@@ -649,9 +746,10 @@ def test_execute_stream_route_falls_back_for_sync_payload(monkeypatch: pytest.Mo
},
)
assert response.status_code == 409
assert response.headers[CONTROL_ACTION_HEADER] == CONTROL_ACTION_PROXY_PUBLIC
assert response.json() == {"action": CONTROL_ACTION_PROXY_PUBLIC}
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
def test_execute_sync_route_handles_gemini_files_list(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -683,7 +781,7 @@ def test_execute_sync_route_handles_gemini_files_list(monkeypatch: pytest.Monkey
monkeypatch.setattr("src.api.public.gemini_files.list_files", _fake_list_files)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
@@ -732,7 +830,7 @@ def test_execute_sync_route_handles_gemini_files_upload_raw_body(
monkeypatch.setattr("src.api.public.gemini_files.upload_file", _fake_upload_file)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
@@ -796,7 +894,7 @@ def test_execute_sync_route_handles_openai_video_remix_with_original_request(
lambda: SimpleNamespace(_check_user_rate_limit=AsyncMock(return_value=None)),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
@@ -860,7 +958,7 @@ def test_plan_stream_route_returns_executor_plan_for_gemini_files_download(
AsyncMock(return_value=fake_plan),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -924,7 +1022,7 @@ def test_plan_stream_route_returns_executor_plan_for_openai_video_content(
AsyncMock(return_value=fake_plan),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -988,7 +1086,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_get(
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -1051,7 +1149,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -1118,7 +1216,7 @@ def test_decision_sync_route_returns_executor_decision_for_openai_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-sync",
json={
@@ -1201,7 +1299,7 @@ def test_decision_stream_route_returns_executor_decision_for_openai_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-stream",
json={
@@ -1247,6 +1345,38 @@ def test_decision_stream_route_returns_executor_decision_for_openai_chat(
}
def test_decision_sync_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_internal_test_client(monkeypatch)
response = client.post(
"/api/internal/gateway/decision-sync",
json={
"trace_id": "trace-guard-decision-sync",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"content-type": "application/json"},
"body_json": {"model": "gpt-5", "messages": []},
},
)
_assert_legacy_guard_response(response)
def test_decision_stream_route_requires_legacy_header(monkeypatch: pytest.MonkeyPatch) -> None:
client = _make_internal_test_client(monkeypatch)
response = client.post(
"/api/internal/gateway/decision-stream",
json={
"trace_id": "trace-guard-decision-stream",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"content-type": "application/json"},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
},
)
_assert_legacy_guard_response(response)
@pytest.mark.parametrize(
("path", "headers", "decision_kind", "client_api_format", "builder_path", "model_name", "mapped_model", "upstream_url"),
[
@@ -1333,7 +1463,7 @@ def test_decision_stream_route_returns_executor_decision_for_claude_and_gemini_c
else {"contents": [{"role": "user", "parts": [{"text": "hello"}]}]}
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-stream",
json={
@@ -1494,7 +1624,7 @@ def test_decision_stream_route_returns_executor_decision_for_cli_variants(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-stream",
json={
@@ -1591,7 +1721,7 @@ def test_decision_sync_route_returns_executor_decision_for_openai_cli_variants(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-sync",
json={
@@ -1708,7 +1838,7 @@ def test_decision_sync_route_returns_executor_decision_for_claude_variants(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-sync",
json={
@@ -1839,7 +1969,7 @@ def test_decision_sync_route_returns_executor_decision_for_gemini_variants(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-sync",
json={
@@ -1946,7 +2076,7 @@ def test_decision_sync_route_returns_executor_decision_for_gemini_files_get(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-sync",
json={
@@ -2097,7 +2227,7 @@ def test_decision_sync_route_returns_executor_decision_for_video_variants(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-sync",
json={
@@ -2198,7 +2328,7 @@ def test_decision_stream_route_returns_executor_decision_for_gemini_files_downlo
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-stream",
json={
@@ -2266,7 +2396,7 @@ def test_decision_stream_route_returns_executor_decision_for_openai_video_conten
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/decision-stream",
json={
@@ -2322,7 +2452,7 @@ def test_plan_sync_route_resolves_auth_context_when_missing(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -2384,7 +2514,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_create(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -2450,7 +2580,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_remix(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -2516,7 +2646,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_video_create(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -2582,7 +2712,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_cancel(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -2646,7 +2776,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_video_delete(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -2710,7 +2840,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_video_cancel(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -2785,7 +2915,7 @@ def test_plan_stream_route_returns_executor_plan_for_openai_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -2860,7 +2990,7 @@ def test_plan_stream_route_resolves_auth_context_when_missing(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -2927,7 +3057,7 @@ def test_plan_stream_route_returns_executor_plan_for_claude_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -2997,7 +3127,7 @@ def test_plan_stream_route_returns_executor_plan_for_gemini_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -3067,7 +3197,7 @@ def test_plan_stream_route_returns_executor_plan_for_openai_cli(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -3143,7 +3273,7 @@ def test_plan_stream_route_returns_executor_plan_for_claude_cli(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -3218,7 +3348,7 @@ def test_plan_stream_route_returns_executor_plan_for_gemini_cli(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-stream",
json={
@@ -3285,7 +3415,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_cli(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3351,7 +3481,7 @@ def test_plan_sync_route_returns_executor_plan_for_openai_compact(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3417,7 +3547,7 @@ def test_plan_sync_route_returns_executor_plan_for_claude_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3483,7 +3613,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_chat(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3549,7 +3679,7 @@ def test_plan_sync_route_returns_executor_plan_for_claude_cli(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3615,7 +3745,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_cli(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3685,7 +3815,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_list(
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3751,7 +3881,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_upload(
AsyncMock(return_value=(fake_plan, {"file_key_id": "file-key-123", "user_id": "user-123"})),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={
@@ -3821,7 +3951,7 @@ def test_plan_sync_route_returns_executor_plan_for_gemini_files_delete(
AsyncMock(return_value=(fake_plan, {})),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/plan-sync",
json={

View File

@@ -5,8 +5,8 @@ from collections.abc import AsyncGenerator
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
from fastapi import Response
from fastapi.responses import StreamingResponse
import src.api.public.gemini_files as gemini_files_mod
@@ -127,12 +127,6 @@ async def test_proxy_request_passes_proxy_snapshot_to_rust_executor(
"execute_sync_json",
_fake_execute_sync_json,
)
monkeypatch.setattr(
gemini_files_mod.HTTPClientPool,
"get_upstream_client",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
response = await gemini_files_mod._proxy_request(
"GET",
"https://generativelanguage.googleapis.com/v1beta/files",
@@ -150,57 +144,30 @@ async def test_proxy_request_passes_proxy_snapshot_to_rust_executor(
@pytest.mark.asyncio
async def test_proxy_request_fallback_uses_upstream_client_proxy_context(
async def test_proxy_request_returns_503_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class _FakeClient:
async def get(self, url: str, headers: dict[str, str]) -> httpx.Response:
assert url == "https://generativelanguage.googleapis.com/v1beta/files"
assert headers == {"x-goog-api-key": "upstream-key"}
return httpx.Response(
200,
request=httpx.Request("GET", url),
json={"files": []},
)
async def _fake_try_rust_sync_proxy_request(*args: object, **kwargs: object) -> None:
async def _fake_try_rust_sync_proxy_request(*args: object, **kwargs: object) -> Response:
del args, kwargs
return None
async def _fake_get_upstream_client(
delegate_cfg: dict[str, object] | None,
*,
proxy_config: dict[str, object] | None = None,
tls_profile: str | None = None,
) -> _FakeClient:
assert delegate_cfg == {"tunnel": True, "node_id": "node-1"}
assert proxy_config == {"enabled": True, "node_id": "node-1"}
assert tls_profile is None
return _FakeClient()
return gemini_files_mod._build_rust_unavailable_response()
monkeypatch.setattr(
gemini_files_mod,
"_try_rust_sync_proxy_request",
_fake_try_rust_sync_proxy_request,
)
monkeypatch.setattr(
gemini_files_mod.HTTPClientPool,
"get_upstream_client",
_fake_get_upstream_client,
)
response = await gemini_files_mod._proxy_request(
"GET",
"https://generativelanguage.googleapis.com/v1beta/files",
{"x-goog-api-key": "upstream-key"},
file_key_id="key-1",
user_id="user-1",
proxy_config={"enabled": True, "node_id": "node-1"},
delegate_config={"tunnel": True, "node_id": "node-1"},
)
assert response.status_code == 200
assert json.loads(response.body) == {"files": []}
body = json.loads(response.body)
assert response.status_code == 503
assert body["error"]["code"] == 503
assert body["error"]["status"] == "UNAVAILABLE"
@pytest.mark.asyncio
@@ -275,13 +242,7 @@ async def test_download_file_uses_enriched_proxy_snapshot_for_regular_files(
"execute_stream",
_fake_execute_stream,
)
monkeypatch.setattr(
gemini_files_mod.HTTPClientPool,
"get_upstream_client",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
response = await gemini_files_mod.download_file(
response = await gemini_files_mod._download_file_response(
"file-1",
SimpleNamespace(
headers={},
@@ -294,3 +255,71 @@ async def test_download_file_uses_enriched_proxy_snapshot_for_regular_files(
body = b"".join([chunk async for chunk in response.body_iterator])
assert body == b"file-bytes"
assert dummy_ctx.closed is True
@pytest.mark.asyncio
async def test_download_file_returns_503_when_rust_stream_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(config, "executor_backend", "rust")
raw_ctx = UpstreamContext(
upstream_key="upstream-key",
base_url="https://generativelanguage.googleapis.com",
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
)
enriched_ctx = UpstreamContext(
upstream_key="upstream-key",
base_url="https://generativelanguage.googleapis.com",
file_key_id="key-1",
user_id="user-1",
provider_id="prov-1",
endpoint_id="ep-1",
)
monkeypatch.setattr(gemini_files_mod, "_extract_gemini_api_key", lambda request: "client-key")
monkeypatch.setattr(
gemini_files_mod,
"create_session",
lambda: _FakeDBContext(SimpleNamespace()),
)
monkeypatch.setattr(
gemini_files_mod.AuthService,
"authenticate_api_key",
lambda db, key: (SimpleNamespace(id="user-1"), SimpleNamespace(id="user-api-key")),
)
monkeypatch.setattr(gemini_files_mod, "_ensure_balance_access", lambda db, user, api_key: None)
monkeypatch.setattr(
gemini_files_mod,
"_resolve_upstream_context",
AsyncMock(return_value=raw_ctx),
)
monkeypatch.setattr(
gemini_files_mod,
"_enrich_upstream_context_proxy",
AsyncMock(return_value=enriched_ctx),
)
async def _fake_execute_stream(self: object, plan: object) -> RustExecutorStreamResult:
del self, plan
raise rust_client_mod.RustExecutorClientError("executor unavailable")
monkeypatch.setattr(
rust_client_mod.RustExecutorClient,
"execute_stream",
_fake_execute_stream,
)
response = await gemini_files_mod._download_file_response(
"file-1",
SimpleNamespace(
headers={},
query_params={"alt": "media"},
),
)
body = json.loads(response.body)
assert response.status_code == 503
assert body["error"]["code"] == 503
assert body["error"]["status"] == "UNAVAILABLE"

View File

@@ -0,0 +1,133 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import gemini_files as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_gemini_files_upload_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"file": {"name": "files/1"}})
response = client.post("/upload/v1beta/files")
assert response.status_code == 200
assert response.json() == {"file": {"name": "files/1"}}
assert calls == [
{
"adapter_type": "PublicGeminiFilesUploadAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_gemini_files_list_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"files": []})
response = client.get("/v1beta/files?pageSize=20&pageToken=next-1")
assert response.status_code == 200
assert response.json() == {"files": []}
assert calls == [
{
"adapter_type": "PublicGeminiFilesListAdapter",
"mode": "public",
"adapter_state": {
"page_size": 20,
"page_token": "next-1",
},
}
]
def test_gemini_files_download_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"download": "ok"})
response = client.get("/v1beta/files/file-1:download?alt=media")
assert response.status_code == 200
assert response.json() == {"download": "ok"}
assert calls == [
{
"adapter_type": "PublicGeminiFilesDownloadAdapter",
"mode": "public",
"adapter_state": {"file_id": "file-1"},
}
]
def test_gemini_files_get_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"name": "files/file-1"})
response = client.get("/v1beta/files/file-1")
assert response.status_code == 200
assert response.json() == {"name": "files/file-1"}
assert calls == [
{
"adapter_type": "PublicGeminiFilesGetAdapter",
"mode": "public",
"adapter_state": {"file_name": "file-1"},
}
]
def test_gemini_files_delete_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"deleted": True})
response = client.delete("/v1beta/files/file-1")
assert response.status_code == 200
assert response.json() == {"deleted": True}
assert calls == [
{
"adapter_type": "PublicGeminiFilesDeleteAdapter",
"mode": "public",
"adapter_state": {"file_name": "file-1"},
}
]

View File

@@ -0,0 +1,117 @@
from __future__ import annotations
from typing import Any
import pytest
from starlette.requests import Request
def _make_request(path: str, method: str = "POST", headers: list[tuple[bytes, bytes]] | None = None) -> Request:
scope = {
"type": "http",
"asgi": {"version": "3.0"},
"http_version": "1.1",
"method": method,
"scheme": "http",
"path": path,
"raw_path": path.encode(),
"query_string": b"",
"headers": headers or [],
"client": ("127.0.0.1", 12345),
"server": ("testserver", 80),
}
return Request(scope)
@pytest.mark.asyncio
@pytest.mark.parametrize(
("route", "model", "stream"),
[
("v1beta_generate", "gemini-2.5-flash", False),
("v1beta_stream", "gemini-2.5-flash", True),
("v1_generate", "gemini-2.5-flash", False),
("v1_stream", "gemini-2.5-flash", True),
],
)
async def test_public_gemini_routes_use_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
route: str,
model: str,
stream: bool,
) -> None:
from src.api.public import gemini as mod
captured: dict[str, Any] = {}
async def fake_run(*, adapter, http_request, db, mode, api_format_hint, path_params, **_kwargs):
captured.update(
{
"adapter": adapter,
"request": http_request,
"db": db,
"mode": mode,
"api_format_hint": api_format_hint,
"path_params": path_params,
}
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
db = object()
request = _make_request(f"/{route}")
if route == "v1beta_generate":
result = await mod.generate_content(model=model, http_request=request, db=db)
elif route == "v1beta_stream":
result = await mod.stream_generate_content(model=model, http_request=request, db=db)
elif route == "v1_generate":
result = await mod.generate_content_v1(model=model, http_request=request, db=db)
else:
result = await mod.stream_generate_content_v1(model=model, http_request=request, db=db)
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.PublicGeminiContentAdapter)
assert captured["adapter"].model == model
assert captured["adapter"].stream is stream
assert captured["request"] is request
assert captured["db"] is db
assert captured["mode"] == captured["adapter"].mode
assert captured["api_format_hint"] == "gemini:chat"
assert captured["path_params"] == {"model": model, "stream": stream}
@pytest.mark.asyncio
async def test_public_gemini_shell_detects_cli_request_for_api_format_hint(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.public import gemini as mod
captured: dict[str, Any] = {}
async def fake_run(*, adapter, http_request, db, mode, api_format_hint, path_params, **_kwargs):
captured.update(
{
"adapter": adapter,
"request": http_request,
"db": db,
"mode": mode,
"api_format_hint": api_format_hint,
"path_params": path_params,
}
)
return {"ok": True}
monkeypatch.setattr(mod.pipeline, "run", fake_run)
request = _make_request(
"/v1beta/models/gemini-2.5-flash:generateContent",
headers=[(b"x-app", b"gemini-cli")],
)
result = await mod.generate_content(model="gemini-2.5-flash", http_request=request, db=object())
assert result == {"ok": True}
assert isinstance(captured["adapter"], mod.PublicGeminiContentAdapter)
assert captured["api_format_hint"] == "gemini:cli"
assert captured["path_params"] == {"model": "gemini-2.5-flash", "stream": False}

View File

@@ -0,0 +1,153 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
module_name: str,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
module = __import__(module_name, fromlist=["router", "pipeline"])
app = FastAPI()
app.include_router(module.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(module.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_public_site_info_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.catalog",
pipeline_result={"site_name": "Aether", "site_subtitle": "AI Gateway"},
)
response = client.get("/api/public/site-info")
assert response.status_code == 200
assert response.json() == {"site_name": "Aether", "site_subtitle": "AI Gateway"}
assert calls == [
{
"adapter_type": "PublicSiteInfoAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_modules_auth_status_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.modules",
pipeline_result=[{"name": "oauth", "display_name": "OAuth", "active": True}],
)
response = client.get("/api/modules/auth-status")
assert response.status_code == 200
assert response.json() == [{"name": "oauth", "display_name": "OAuth", "active": True}]
assert calls == [
{
"adapter_type": "PublicAuthModulesStatusAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_capabilities_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.capabilities",
pipeline_result={"capabilities": []},
)
response = client.get("/api/capabilities")
assert response.status_code == 200
assert response.json() == {"capabilities": []}
assert calls == [
{
"adapter_type": "PublicCapabilitiesListAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_user_configurable_capabilities_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.capabilities",
pipeline_result={"capabilities": [{"name": "vision"}]},
)
response = client.get("/api/capabilities/user-configurable")
assert response.status_code == 200
assert response.json() == {"capabilities": [{"name": "vision"}]}
assert calls == [
{
"adapter_type": "PublicUserConfigurableCapabilitiesAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_public_model_capabilities_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(
monkeypatch,
"src.api.public.capabilities",
pipeline_result={"model": "gpt-5", "supported_capabilities": []},
)
response = client.get("/api/capabilities/model/gpt-5")
assert response.status_code == 200
assert response.json() == {"model": "gpt-5", "supported_capabilities": []}
assert calls == [
{
"adapter_type": "PublicModelCapabilitiesAdapter",
"mode": "public",
"adapter_state": {"model_name": "gpt-5"},
}
]

View File

@@ -0,0 +1,130 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import models as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_public_openai_models_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"object": "list", "data": []})
response = client.get("/v1/models?after_id=model-a&limit=12")
assert response.status_code == 200
assert response.json() == {"object": "list", "data": []}
assert calls == [
{
"adapter_type": "PublicModelsListAdapter",
"mode": "public",
"adapter_state": {
"before_id": None,
"after_id": "model-a",
"limit": 12,
"page_size": 50,
"page_token": None,
},
}
]
def test_public_model_detail_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"id": "gpt-5"})
response = client.get("/v1/models/gpt-5")
assert response.status_code == 200
assert response.json() == {"id": "gpt-5"}
assert calls == [
{
"adapter_type": "PublicModelDetailAdapter",
"mode": "public",
"adapter_state": {
"model_id": "gpt-5",
"force_gemini_name": False,
},
}
]
def test_public_gemini_models_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"models": []})
response = client.get("/v1beta/models?pageSize=25&pageToken=next-1")
assert response.status_code == 200
assert response.json() == {"models": []}
assert calls == [
{
"adapter_type": "PublicModelsListAdapter",
"mode": "public",
"adapter_state": {
"before_id": None,
"after_id": None,
"limit": 20,
"page_size": 25,
"page_token": "next-1",
},
}
]
def test_public_gemini_model_detail_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"name": "models/gemini-2.5-pro"})
response = client.get("/v1beta/models/models/gemini-2.5-pro")
assert response.status_code == 200
assert response.json() == {"name": "models/gemini-2.5-pro"}
assert calls == [
{
"adapter_type": "PublicModelDetailAdapter",
"mode": "public",
"adapter_state": {
"model_id": "models/gemini-2.5-pro",
"force_gemini_name": True,
},
}
]

View File

@@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock
import httpx
import pytest
from fastapi import HTTPException
def _build_provider_fixture() -> SimpleNamespace:
@@ -65,10 +66,7 @@ async def test_test_connection_prefers_rust_executor(
rust_call = AsyncMock(return_value=rust_response)
monkeypatch.setattr(mod, "_try_rust_test_connection_response", rust_call)
get_upstream_client = AsyncMock(side_effect=AssertionError("python upstream client should not be used"))
monkeypatch.setattr(mod.HTTPClientPool, "get_upstream_client", get_upstream_client)
result = await mod.test_connection(
result = await mod._test_connection_response(
request=SimpleNamespace(query_params={}),
db=MagicMock(),
provider=None,
@@ -79,11 +77,10 @@ async def test_test_connection_prefers_rust_executor(
assert result["status"] == "success"
assert result["response_id"] == "resp_rust"
rust_call.assert_awaited_once()
get_upstream_client.assert_not_awaited()
@pytest.mark.asyncio
async def test_test_connection_fallback_uses_transport_aware_client(
async def test_test_connection_returns_503_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.api.public import system_catalog as mod
@@ -105,38 +102,29 @@ async def test_test_connection_fallback_uses_transport_aware_client(
lambda *_args, **_kwargs: "https://upstream.test/v1/chat/completions",
)
proxy_config = {"enabled": True, "url": "http://proxy.test:8080"}
delegate_cfg = {"node_id": "node-1", "tunnel": True}
monkeypatch.setattr(
mod,
"_build_test_connection_transport_context",
AsyncMock(return_value=(proxy_config, delegate_cfg, None)),
AsyncMock(return_value=({"enabled": True}, {"node_id": "node-1", "tunnel": True}, None)),
)
monkeypatch.setattr(mod, "_try_rust_test_connection_response", AsyncMock(return_value=None))
monkeypatch.setattr(
mod,
"_try_rust_test_connection_response",
AsyncMock(
side_effect=HTTPException(
status_code=503,
detail="System catalog test-connection requires Rust executor",
)
),
)
with pytest.raises(HTTPException) as exc_info:
await mod._test_connection_response(
request=SimpleNamespace(query_params={}),
db=MagicMock(),
provider=None,
model="gpt-test",
api_format=None,
)
upstream_response = httpx.Response(
200,
request=httpx.Request("POST", "https://upstream.test/v1/chat/completions"),
json={"id": "resp_python"},
)
upstream_client = MagicMock()
upstream_client.post = AsyncMock(return_value=upstream_response)
get_upstream_client = AsyncMock(return_value=upstream_client)
monkeypatch.setattr(mod.HTTPClientPool, "get_upstream_client", get_upstream_client)
result = await mod.test_connection(
request=SimpleNamespace(query_params={}),
db=MagicMock(),
provider=None,
model="gpt-test",
api_format=None,
)
assert result["status"] == "success"
assert result["response_id"] == "resp_python"
get_upstream_client.assert_awaited_once_with(delegate_cfg, proxy_config=proxy_config)
upstream_client.post.assert_awaited_once_with(
"https://upstream.test/v1/chat/completions",
json={"model": "gpt-test"},
headers={"authorization": "Bearer test"},
)
assert exc_info.value.status_code == 503
assert exc_info.value.detail == "System catalog test-connection requires Rust executor"

View File

@@ -0,0 +1,164 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(
monkeypatch: pytest.MonkeyPatch,
*,
pipeline_result: Any,
) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import system_catalog as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str | None = None,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db, api_format_hint, path_params
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"adapter_state": dict(getattr(adapter, "__dict__", {})),
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_system_catalog_health_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "ok"})
response = client.get("/v1/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
assert calls == [
{
"adapter_type": "PublicServiceHealthAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_system_catalog_simple_health_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "healthy"})
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}
assert calls == [
{
"adapter_type": "PublicSimpleHealthCheckAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_system_catalog_root_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "running"})
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"status": "running"}
assert calls == [
{
"adapter_type": "PublicRootCatalogAdapter",
"mode": "public",
"adapter_state": {},
}
]
def test_system_catalog_provider_list_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"providers": []})
response = client.get("/v1/providers?include_models=true&include_endpoints=true&active_only=false")
assert response.status_code == 200
assert response.json() == {"providers": []}
assert calls == [
{
"adapter_type": "PublicProvidersListAdapter",
"mode": "public",
"adapter_state": {
"include_models": True,
"include_endpoints": True,
"active_only": False,
},
}
]
def test_system_catalog_provider_detail_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"id": "provider-1"})
response = client.get("/v1/providers/provider-1?include_models=true")
assert response.status_code == 200
assert response.json() == {"id": "provider-1"}
assert calls == [
{
"adapter_type": "PublicProviderDetailAdapter",
"mode": "public",
"adapter_state": {
"provider_identifier": "provider-1",
"include_models": True,
"include_endpoints": False,
},
}
]
def test_system_catalog_test_connection_route_is_pipeline_shell(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"status": "success"})
response = client.get("/v1/test-connection?provider=openai&model=gpt-5&api_format=openai:chat")
assert response.status_code == 200
assert response.json() == {"status": "success"}
assert calls == [
{
"adapter_type": "PublicTestConnectionAdapter",
"mode": "public",
"adapter_state": {
"provider": "openai",
"model": "gpt-5",
"api_format": "openai:chat",
},
}
]

View File

@@ -0,0 +1,123 @@
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.database import get_db
def _build_app(monkeypatch: pytest.MonkeyPatch, *, pipeline_result: Any) -> tuple[TestClient, list[dict[str, Any]]]:
from src.api.public import videos as mod
app = FastAPI()
app.include_router(mod.router)
app.dependency_overrides[get_db] = lambda: MagicMock()
calls: list[dict[str, Any]] = []
async def _fake_pipeline_run(
*,
adapter: Any,
http_request: object,
db: object,
mode: object,
api_format_hint: str,
path_params: dict[str, Any] | None = None,
) -> Any:
del http_request, db
calls.append(
{
"adapter_type": type(adapter).__name__,
"mode": getattr(mode, "value", mode),
"api_format_hint": api_format_hint,
"path_params": path_params,
}
)
return pipeline_result
monkeypatch.setattr(mod.pipeline, "run", _fake_pipeline_run)
return TestClient(app), calls
def test_openai_video_create_route_is_pipeline_shell(monkeypatch: pytest.MonkeyPatch) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"ok": True})
response = client.post("/v1/videos", json={"model": "sora", "prompt": "hello"})
assert response.status_code == 200
assert response.json() == {"ok": True}
assert calls == [
{
"adapter_type": "OpenAIVideoAdapter",
"mode": "standard",
"api_format_hint": "openai:video",
"path_params": None,
}
]
def test_openai_video_download_route_passes_task_id_to_pipeline(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"download": True})
response = client.get("/v1/videos/task-123/content")
assert response.status_code == 200
assert response.json() == {"download": True}
assert calls == [
{
"adapter_type": "OpenAIVideoAdapter",
"mode": "standard",
"api_format_hint": "openai:video",
"path_params": {"task_id": "task-123"},
}
]
def test_gemini_video_create_route_passes_model_to_pipeline(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"ok": True})
response = client.post(
"/v1beta/models/veo-3:predictLongRunning",
json={"prompt": "hello"},
)
assert response.status_code == 200
assert response.json() == {"ok": True}
assert calls == [
{
"adapter_type": "GeminiVeoAdapter",
"mode": "standard",
"api_format_hint": "gemini:video",
"path_params": {"model": "veo-3"},
}
]
def test_gemini_video_cancel_route_reconstructs_operation_name(
monkeypatch: pytest.MonkeyPatch,
) -> None:
client, calls = _build_app(monkeypatch, pipeline_result={"ok": True})
response = client.post("/v1beta/models/veo-3/operations/op-1:cancel")
assert response.status_code == 200
assert response.json() == {"ok": True}
assert calls == [
{
"adapter_type": "GeminiVeoAdapter",
"mode": "standard",
"api_format_hint": "gemini:video",
"path_params": {
"task_id": "models/veo-3/operations/op-1",
"action": "cancel",
},
}
]

View File

@@ -4,7 +4,7 @@ import json
import time
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, Mock
import pytest
from fastapi import BackgroundTasks, FastAPI
@@ -77,6 +77,14 @@ def _wait_until(predicate: Any, *, timeout: float = 1.0, interval: float = 0.01)
assert predicate()
def _make_legacy_test_client(app: FastAPI) -> TestClient:
return TestClient(
app,
base_url="http://127.0.0.1",
headers={"x-aether-legacy-internal-gateway": "true"},
)
def test_build_gateway_sync_telemetry_writer_uses_queue_writer_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -316,7 +324,7 @@ def test_auth_context_route_returns_openai_bearer_auth_context(
),
)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/auth-context",
json={
@@ -373,14 +381,17 @@ def test_execute_sync_route_returns_controlled_response(monkeypatch: pytest.Monk
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"headers": {
"user-agent": "pytest",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
@@ -440,7 +451,7 @@ def test_execute_sync_route_resolves_auth_context_when_missing(
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
@@ -450,6 +461,7 @@ def test_execute_sync_route_resolves_auth_context_when_missing(
"headers": {
"user-agent": "pytest",
"authorization": "Bearer client-key",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": []},
},
@@ -467,7 +479,7 @@ def test_execute_sync_route_falls_back_for_stream_payload() -> None:
monkeypatch = pytest.MonkeyPatch()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
@@ -484,12 +496,235 @@ def test_execute_sync_route_falls_back_for_stream_payload() -> None:
},
)
assert response.status_code == 409
assert response.headers[CONTROL_ACTION_HEADER] == CONTROL_ACTION_PROXY_PUBLIC
assert response.json() == {"action": CONTROL_ACTION_PROXY_PUBLIC}
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
monkeypatch.undo()
def test_execute_sync_route_requires_explicit_chat_cli_opt_in(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
resolve_sync_adapter = Mock(return_value=(Mock(), {}))
monkeypatch.setattr("src.api.internal.gateway._resolve_gateway_sync_adapter", resolve_sync_adapter)
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-no-opt-in",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
resolve_sync_adapter.assert_not_called()
def test_execute_sync_route_requires_legacy_internal_gateway_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
resolve_sync_adapter = Mock(return_value=(Mock(), {}))
monkeypatch.setattr("src.api.internal.gateway._resolve_gateway_sync_adapter", resolve_sync_adapter)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/execute-sync",
json={
"trace_id": "trace-sync-no-legacy-header",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {
"user-agent": "pytest",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
resolve_sync_adapter.assert_not_called()
def test_decision_sync_route_requires_legacy_internal_gateway_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
build_decision = AsyncMock(return_value=MagicMock())
monkeypatch.setattr("src.api.internal.gateway._build_openai_chat_sync_decision", build_decision)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/decision-sync",
json={
"trace_id": "trace-decision-no-legacy-header",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {
"content-type": "application/json",
"authorization": "Bearer client-key",
},
"body_json": {"model": "gpt-5", "messages": []},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
build_decision.assert_not_awaited()
def test_plan_stream_route_requires_legacy_internal_gateway_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
build_plan = AsyncMock(return_value=MagicMock())
monkeypatch.setattr("src.api.internal.gateway._build_openai_chat_stream_plan", build_plan)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/plan-stream",
json={
"trace_id": "trace-plan-no-legacy-header",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {
"content-type": "application/json",
"authorization": "Bearer client-key",
},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
"auth_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"access_allowed": True,
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
build_plan.assert_not_awaited()
def test_finalize_sync_route_requires_legacy_internal_gateway_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
finalize_mock = AsyncMock(return_value=JSONResponse(content={"ok": True}))
monkeypatch.setattr("src.api.internal.gateway._finalize_gateway_chat_sync", finalize_mock)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/finalize-sync",
json={
"trace_id": "trace-finalize-no-legacy-header",
"report_kind": "openai_chat_sync_finalize",
"report_context": {
"user_id": "user-123",
"api_key_id": "key-123",
"client_api_format": "openai:chat",
},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {"id": "upstream-123"},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
finalize_mock.assert_not_awaited()
def test_report_sync_route_requires_legacy_internal_gateway_header(
monkeypatch: pytest.MonkeyPatch,
) -> None:
app = FastAPI()
app.include_router(router)
app.dependency_overrides[get_db] = lambda: object()
monkeypatch.setattr("src.api.internal.gateway.ensure_loopback", lambda request: None)
record_mock = AsyncMock(return_value=None)
monkeypatch.setattr(
"src.api.internal.gateway._record_gateway_openai_chat_sync_success",
record_mock,
)
client = TestClient(app, base_url="http://127.0.0.1")
response = client.post(
"/api/internal/gateway/report-sync",
json={
"trace_id": "trace-report-no-legacy-header",
"report_kind": "openai_chat_sync_success",
"report_context": {"user_id": "user-123", "api_key_id": "key-123"},
"status_code": 200,
"headers": {"content-type": "application/json"},
"body_json": {
"id": "chatcmpl-123",
"object": "chat.completion",
"choices": [],
},
},
)
assert response.status_code == 410
assert response.json() == {
"detail": "legacy internal gateway route removed; use public proxy"
}
assert record_mock.await_count == 0
def test_execute_stream_route_returns_controlled_stream(monkeypatch: pytest.MonkeyPatch) -> None:
app = FastAPI()
app.include_router(router)
@@ -524,14 +759,17 @@ def test_execute_stream_route_returns_controlled_stream(monkeypatch: pytest.Monk
)
monkeypatch.setattr("src.api.internal.gateway.get_pipeline", lambda: fake_pipeline)
client = TestClient(app, base_url="http://127.0.0.1")
client = _make_legacy_test_client(app)
response = client.post(
"/api/internal/gateway/execute-stream",
json={
"trace_id": "trace-stream-123",
"method": "POST",
"path": "/v1/chat/completions",
"headers": {"user-agent": "pytest"},
"headers": {
"user-agent": "pytest",
"x-aether-control-execute-fallback": "true",
},
"body_json": {"model": "gpt-5", "messages": [], "stream": True},
"auth_context": {
"user_id": "user-123",

View File

@@ -163,9 +163,16 @@ class TestPipelineAuditLogging:
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/v1/messages"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1/messages"
mock_context.original_headers = {}
mock_context.start_time = 1000.0
mock_adapter = MagicMock()
@@ -199,9 +206,16 @@ class TestPipelineAuditLogging:
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/v1/messages"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1/messages"
mock_context.original_headers = {}
mock_context.start_time = 1000.0
mock_adapter = MagicMock()
@@ -239,11 +253,20 @@ class TestPipelineAuditLogging:
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/api/auth/refresh"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/api/auth/refresh"
mock_context.request.state = SimpleNamespace(tx_committed_by_route=True)
mock_context.original_headers = {}
mock_context.start_time = 1000.0
mock_context.sync_runtime_state_from_request = MagicMock(
side_effect=lambda: setattr(mock_context, "tx_committed_by_route", True)
)
mock_adapter = MagicMock()
mock_adapter.name = "test-adapter"
@@ -261,6 +284,8 @@ class TestPipelineAuditLogging:
"""测试没有数据库会话时跳过审计"""
mock_context = MagicMock()
mock_context.db = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.tx_committed_by_route = False
mock_adapter = MagicMock()
mock_adapter.audit_log_enabled = True
@@ -302,10 +327,17 @@ class TestPipelineAuditLogging:
mock_context.request_id = "req-123"
mock_context.client_ip = "127.0.0.1"
mock_context.user_agent = "test-agent"
mock_context.request_method = "POST"
mock_context.request_path = "/v1/messages"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = None
mock_context.rate_limit_scope = None
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1/messages"
mock_context.start_time = 1000.0
mock_context.original_headers = {}
mock_adapter = MagicMock()
mock_adapter.name = "test-adapter"
@@ -321,6 +353,81 @@ class TestPipelineAuditLogging:
# 不应该抛出异常
pipeline._record_audit_event(mock_context, mock_adapter, success=True)
def test_build_audit_metadata_prefers_context_path_params(
self, pipeline: ApiRequestPipeline
) -> None:
mock_context = MagicMock()
mock_context.start_time = 1000.0
mock_context.mode = "standard"
mock_context.api_format_hint = "gemini"
mock_context.query_params = {}
mock_context.raw_body = b"{}"
mock_context.balance_remaining = 12.5
mock_context.audit_metadata = {}
mock_context.quiet_logging = False
mock_context.user = None
mock_context.api_key = None
mock_context.request_method = "POST"
mock_context.request_path = "/v1beta/models/gemini-2.5-flash:streamGenerateContent"
mock_context.tx_committed_by_route = False
mock_context.gateway_execution_path = "executor_local"
mock_context.rate_limit_scope = "user"
mock_context.sync_runtime_state_from_request = MagicMock()
mock_context.path_params = {"model": "gemini-2.5-flash", "stream": True}
mock_context.request = MagicMock()
mock_context.request.method = "POST"
mock_context.request.url.path = "/v1beta/models/gemini-2.5-flash:streamGenerateContent"
mock_context.original_headers = {"content-type": "application/json"}
mock_adapter = MagicMock()
mock_adapter.name = "public.gemini.content"
mock_adapter.__class__.__name__ = "PublicGeminiContentAdapter"
mock_adapter.mode.value = "standard"
mock_adapter.get_audit_metadata.return_value = {}
with patch("time.time", return_value=1001.0):
metadata = pipeline._build_audit_metadata(
context=mock_context,
adapter=mock_adapter,
success=True,
status_code=200,
error=None,
)
assert metadata["path_params"] == {"model": "gemini-2.5-flash", "stream": True}
assert metadata["gateway_execution_path"] == "executor_local"
assert metadata["rate_limit_scope"] == "user"
@pytest.mark.asyncio
async def test_apply_context_runtime_state_prefers_snapshot_balance(
self, pipeline: ApiRequestPipeline
) -> None:
context = MagicMock()
context.prefetched_balance_remaining = 9.5
context.balance_remaining = None
context.management_token = None
context.quiet_logging = False
auth_state = SimpleNamespace(
user=MagicMock(id="user-1"),
api_key=MagicMock(id="key-1"),
management_token=None,
)
pipeline._calculate_balance_remaining_async = AsyncMock(
side_effect=AssertionError("prefetched snapshot should skip balance lookup")
)
await pipeline._apply_context_runtime_state_legacy(
context,
mode=ApiMode.STANDARD,
auth_state=auth_state,
quiet=True,
)
assert context.balance_remaining == 9.5
assert context.quiet_logging is True
class TestPipelineAuthentication:
"""测试 Pipeline 认证相关逻辑"""
@@ -630,12 +737,95 @@ class TestPipelineAuthentication:
with pytest.raises(BalanceInsufficientException):
await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
@pytest.mark.asyncio
async def test_authenticate_client_gateway_marker_without_trusted_ids_falls_back_to_legacy(
self, pipeline: ApiRequestPipeline
) -> None:
mock_user = MagicMock()
mock_user.id = "user-123"
mock_api_key = MagicMock()
mock_api_key.id = "key-123"
mock_request = MagicMock()
mock_request.headers = {"x-aether-gateway": "rust-phase3b"}
mock_request.client = SimpleNamespace(host="127.0.0.1")
mock_request.url.path = "/v1/chat/completions"
mock_request.state = MagicMock()
db_user = MagicMock()
db_user.id = "user-123"
db_user.is_active = True
db_user.is_deleted = False
db_api_key = MagicMock()
db_api_key.id = "key-123"
db_api_key.user_id = "user-123"
db_api_key.is_active = True
db_api_key.is_locked = False
db_api_key.is_standalone = False
db_api_key.expires_at = None
mock_db = MagicMock()
user_query = MagicMock()
user_query.filter.return_value.first.return_value = db_user
api_key_query = MagicMock()
api_key_query.filter.return_value.first.return_value = db_api_key
mock_db.query.side_effect = [user_query, api_key_query]
mock_adapter = MagicMock()
mock_adapter.extract_api_key = MagicMock(return_value="sk-test")
with patch.object(
pipeline.auth_service,
"authenticate_api_key_threadsafe",
new_callable=AsyncMock,
return_value=MagicMock(
user=mock_user,
api_key=mock_api_key,
access_allowed=True,
balance_remaining=12.5,
),
) as mock_auth:
user, api_key = await pipeline._authenticate_client(mock_request, mock_db, mock_adapter)
assert user == db_user
assert api_key == db_api_key
mock_adapter.extract_api_key.assert_called_once_with(mock_request)
mock_auth.assert_awaited_once_with("sk-test")
assert mock_request.state.prefetched_balance_remaining == 12.5
class TestPipelineUserRateLimit:
@pytest.fixture
def pipeline(self) -> ApiRequestPipeline:
return ApiRequestPipeline()
@pytest.mark.asyncio
async def test_apply_legacy_request_guards_skips_when_rust_completed_preflight(
self, pipeline: ApiRequestPipeline
) -> None:
request = MagicMock()
request.headers = {
"x-aether-gateway": "rust-phase3b",
"x-aether-rate-limit-preflight": "true",
}
request.client = SimpleNamespace(host="127.0.0.1")
request.state = MagicMock()
db = MagicMock()
user = MagicMock(id="user-1")
api_key = MagicMock(id="key-1")
auth_state = SimpleNamespace(user=user, api_key=api_key, management_token=None)
pipeline._check_user_rate_limit = AsyncMock(
side_effect=AssertionError("trusted rust preflight should skip legacy limiter")
)
await pipeline._apply_legacy_request_guards(
request,
db,
mode=ApiMode.STANDARD,
auth_state=auth_state,
)
@pytest.mark.asyncio
async def test_check_user_rate_limit_uses_system_default_for_user_scope(
self, pipeline: ApiRequestPipeline, monkeypatch: pytest.MonkeyPatch
@@ -1126,6 +1316,65 @@ class TestPipelineAdminAuth:
assert management_token is None
mock_db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_authenticate_admin_uses_identity_helper_for_session_touch(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-123"
mock_user = MagicMock()
mock_user.id = "admin-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.role = UserRole.ADMIN
mock_user.email = "admin@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-admin-123",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "admin-123",
"created_at": created_at.isoformat(),
"session_id": "session-123",
},
),
patch(
"src.api.base.pipeline.get_request_identity_metadata",
return_value=MagicMock(client_ip="203.0.113.10", user_agent="admin-agent/1.0"),
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch(
"src.api.base.pipeline.SessionService.touch_session",
return_value=True,
) as mock_touch,
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
await pipeline._authenticate_admin(mock_request, mock_db)
mock_touch.assert_called_once_with(
mock_session,
client_ip="203.0.113.10",
user_agent="admin-agent/1.0",
)
class TestPipelineUserAuth:
"""测试普通用户 JWT 认证"""
@@ -1318,3 +1567,61 @@ class TestPipelineUserAuth:
assert user == mock_user
assert management_token is None
mock_db.commit.assert_not_called()
@pytest.mark.asyncio
async def test_authenticate_user_uses_identity_helper_for_session_touch(
self, pipeline: ApiRequestPipeline
) -> None:
created_at = datetime.now(timezone.utc)
mock_session = MagicMock()
mock_session.id = "session-456"
mock_user = MagicMock()
mock_user.id = "user-123"
mock_user.is_active = True
mock_user.is_deleted = False
mock_user.email = "user@example.com"
mock_user.created_at = created_at
mock_request = MagicMock()
mock_request.headers = {
"authorization": "Bearer valid-token",
"X-Client-Device-Id": "device-user-456",
}
mock_request.state = MagicMock()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_user
with (
patch.object(
pipeline.auth_service,
"verify_token",
new_callable=AsyncMock,
return_value={
"user_id": "user-123",
"created_at": created_at.isoformat(),
"session_id": "session-456",
},
),
patch(
"src.api.base.pipeline.get_request_identity_metadata",
return_value=MagicMock(client_ip="198.51.100.25", user_agent="user-agent/2.0"),
),
patch(
"src.api.base.pipeline.SessionService.get_active_session",
return_value=mock_session,
),
patch(
"src.api.base.pipeline.SessionService.touch_session",
return_value=True,
) as mock_touch,
patch("src.api.base.pipeline.SessionService.assert_session_device_matches"),
):
await pipeline._authenticate_user(mock_request, mock_db)
mock_touch.assert_called_once_with(
mock_session,
client_ip="198.51.100.25",
user_agent="user-agent/2.0",
)

View File

@@ -0,0 +1,38 @@
from __future__ import annotations
import importlib
import sys
def test_python_host_import_does_not_load_public_compat_modules() -> None:
targets = [
"src.main",
"src.api.public",
"src.api.public.compat",
"src.api.public.support",
"src.api.public.models",
"src.api.public.capabilities",
"src.api.public.modules",
"src.api.public.openai",
"src.api.public.claude",
"src.api.public.gemini",
"src.api.public.videos",
"src.api.public.gemini_files",
"src.api.public.system_catalog",
]
for name in targets:
sys.modules.pop(name, None)
importlib.import_module("src.main")
assert "src.api.public.support" in sys.modules
assert "src.api.public.compat" not in sys.modules
assert "src.api.public.models" not in sys.modules
assert "src.api.public.capabilities" not in sys.modules
assert "src.api.public.modules" not in sys.modules
assert "src.api.public.openai" not in sys.modules
assert "src.api.public.claude" not in sys.modules
assert "src.api.public.gemini" not in sys.modules
assert "src.api.public.videos" not in sys.modules
assert "src.api.public.gemini_files" not in sys.modules
assert "src.api.public.system_catalog" not in sys.modules

View File

@@ -0,0 +1,920 @@
from __future__ import annotations
from starlette.routing import Match
from src.api.announcements import router as python_announcement_router
from src.api.admin import python_admin_router
from src.api.auth import router as python_auth_router
from src.api.dashboard import router as python_dashboard_router
import src.api.internal as internal_module
from src.api.internal.gateway import router as legacy_gateway_bridge_router
from src.api.monitoring import router as python_monitoring_router
from src.api.payment import router as python_payment_router
from src.api.public import frontdoor_compat_router, router as python_public_router
from src.api.user_me import router as python_user_me_router
from src.api.wallet import router as python_wallet_router
import src.main as main_module
LEGACY_GATEWAY_BRIDGE_PATH_PREFIX = "/api/internal/gateway"
RUST_OWNED_ADMIN_PATHS = {
"/api/admin/modules/status",
"/api/admin/modules/status/{module_name}",
"/api/admin/modules/status/{module_name}/enabled",
"/api/admin/system/version",
"/api/admin/system/check-update",
"/api/admin/system/aws-regions",
"/api/admin/system/stats",
"/api/admin/system/settings",
"/api/admin/system/config/export",
"/api/admin/system/users/export",
"/api/admin/system/config/import",
"/api/admin/system/users/import",
"/api/admin/system/smtp/test",
"/api/admin/system/cleanup",
"/api/admin/system/purge/config",
"/api/admin/system/purge/users",
"/api/admin/system/purge/usage",
"/api/admin/system/purge/audit-logs",
"/api/admin/system/purge/request-bodies",
"/api/admin/system/purge/stats",
"/api/admin/system/configs",
"/api/admin/system/configs/{key}",
"/api/admin/system/api-formats",
"/api/admin/system/email/templates",
"/api/admin/system/email/templates/{template_type}",
"/api/admin/providers/",
"/api/admin/providers/summary",
"/api/admin/providers/{provider_id}",
"/api/admin/providers/{provider_id}/summary",
"/api/admin/providers/{provider_id}/health-monitor",
"/api/admin/providers/{provider_id}/mapping-preview",
"/api/admin/providers/{provider_id}/delete-task/{task_id}",
"/api/admin/providers/{provider_id}/pool-status",
"/api/admin/providers/{provider_id}/pool/clear-cooldown/{key_id}",
"/api/admin/providers/{provider_id}/pool/reset-cost/{key_id}",
"/api/admin/providers/{provider_id}/models",
"/api/admin/providers/{provider_id}/models/{model_id}",
"/api/admin/providers/{provider_id}/models/batch",
"/api/admin/providers/{provider_id}/available-source-models",
"/api/admin/providers/{provider_id}/assign-global-models",
"/api/admin/providers/{provider_id}/import-from-upstream",
"/api/admin/endpoints/providers/{provider_id}/endpoints",
"/api/admin/endpoints/defaults/{api_format}/body-rules",
"/api/admin/endpoints/{endpoint_id}",
"/api/admin/endpoints/keys/{key_id}",
"/api/admin/endpoints/keys/grouped-by-format",
"/api/admin/endpoints/keys/{key_id}/reveal",
"/api/admin/endpoints/keys/{key_id}/export",
"/api/admin/endpoints/keys/batch-delete",
"/api/admin/endpoints/keys/{key_id}/clear-oauth-invalid",
"/api/admin/endpoints/providers/{provider_id}/keys",
"/api/admin/endpoints/providers/{provider_id}/refresh-quota",
"/api/admin/endpoints/rpm/key/{key_id}",
"/api/admin/endpoints/health/summary",
"/api/admin/endpoints/health/status",
"/api/admin/endpoints/health/api-formats",
"/api/admin/endpoints/health/key/{key_id}",
"/api/admin/endpoints/health/keys/{key_id}",
"/api/admin/endpoints/health/keys",
"/api/admin/provider-oauth/supported-types",
"/api/admin/provider-oauth/keys/{key_id}/start",
"/api/admin/provider-oauth/keys/{key_id}/complete",
"/api/admin/provider-oauth/keys/{key_id}/refresh",
"/api/admin/provider-oauth/providers/{provider_id}/start",
"/api/admin/provider-oauth/providers/{provider_id}/complete",
"/api/admin/provider-oauth/providers/{provider_id}/import-refresh-token",
"/api/admin/provider-oauth/providers/{provider_id}/device-authorize",
"/api/admin/provider-oauth/providers/{provider_id}/device-poll",
"/api/admin/provider-oauth/providers/{provider_id}/batch-import",
"/api/admin/provider-oauth/providers/{provider_id}/batch-import/tasks",
"/api/admin/provider-oauth/providers/{provider_id}/batch-import/tasks/{task_id}",
"/api/admin/adaptive/keys",
"/api/admin/adaptive/keys/{key_id}/mode",
"/api/admin/adaptive/keys/{key_id}/stats",
"/api/admin/adaptive/keys/{key_id}/learning",
"/api/admin/adaptive/keys/{key_id}/limit",
"/api/admin/adaptive/summary",
"/api/admin/provider-ops/architectures",
"/api/admin/provider-ops/architectures/{architecture_id}",
"/api/admin/provider-ops/providers/{provider_id}/status",
"/api/admin/provider-ops/providers/{provider_id}/config",
"/api/admin/provider-ops/providers/{provider_id}/connect",
"/api/admin/provider-ops/providers/{provider_id}/disconnect",
"/api/admin/provider-ops/providers/{provider_id}/verify",
"/api/admin/provider-ops/providers/{provider_id}/actions/{action_type}",
"/api/admin/provider-ops/providers/{provider_id}/balance",
"/api/admin/provider-ops/providers/{provider_id}/checkin",
"/api/admin/provider-ops/batch/balance",
"/api/admin/billing/presets",
"/api/admin/billing/presets/apply",
"/api/admin/billing/rules",
"/api/admin/billing/rules/{rule_id}",
"/api/admin/billing/collectors",
"/api/admin/billing/collectors/{collector_id}",
"/api/admin/provider-strategy/providers/{provider_id}/billing",
"/api/admin/provider-strategy/providers/{provider_id}/stats",
"/api/admin/provider-strategy/strategies",
"/api/admin/provider-strategy/providers/{provider_id}/quota",
"/api/admin/provider-query/models",
"/api/admin/provider-query/test-model",
"/api/admin/provider-query/test-model-failover",
"/api/admin/payments/orders",
"/api/admin/payments/orders/{order_id}",
"/api/admin/payments/orders/{order_id}/expire",
"/api/admin/payments/orders/{order_id}/credit",
"/api/admin/payments/orders/{order_id}/fail",
"/api/admin/payments/callbacks",
"/api/admin/security/ip/blacklist",
"/api/admin/security/ip/blacklist/{ip_address}",
"/api/admin/security/ip/blacklist/stats",
"/api/admin/security/ip/whitelist",
"/api/admin/security/ip/whitelist/{ip_address}",
"/api/admin/security/ip/whitelist",
"/api/admin/stats/providers/quota-usage",
"/api/admin/stats/comparison",
"/api/admin/stats/errors/distribution",
"/api/admin/stats/performance/percentiles",
"/api/admin/stats/cost/forecast",
"/api/admin/stats/cost/savings",
"/api/admin/stats/leaderboard/api-keys",
"/api/admin/stats/leaderboard/models",
"/api/admin/stats/leaderboard/users",
"/api/admin/stats/time-series",
"/api/admin/monitoring/audit-logs",
"/api/admin/monitoring/system-status",
"/api/admin/monitoring/suspicious-activities",
"/api/admin/monitoring/user-behavior/{user_id}",
"/api/admin/monitoring/resilience-status",
"/api/admin/monitoring/resilience/circuit-history",
"/api/admin/monitoring/resilience/error-stats",
"/api/admin/monitoring/trace/{request_id}",
"/api/admin/monitoring/trace/stats/provider/{provider_id}",
"/api/admin/monitoring/cache/stats",
"/api/admin/monitoring/cache/affinity/{user_identifier}",
"/api/admin/monitoring/cache/affinities",
"/api/admin/monitoring/cache/users/{user_identifier}",
"/api/admin/monitoring/cache/affinity/{affinity_key}/{endpoint_id}/{model_id}/{api_format}",
"/api/admin/monitoring/cache",
"/api/admin/monitoring/cache/providers/{provider_id}",
"/api/admin/monitoring/cache/config",
"/api/admin/monitoring/cache/metrics",
"/api/admin/monitoring/cache/model-mapping/stats",
"/api/admin/monitoring/cache/model-mapping",
"/api/admin/monitoring/cache/model-mapping/{model_name}",
"/api/admin/monitoring/cache/model-mapping/provider/{provider_id}/{global_model_id}",
"/api/admin/monitoring/cache/redis-keys",
"/api/admin/monitoring/cache/redis-keys/{category}",
"/api/admin/usage/aggregation/stats",
"/api/admin/usage/stats",
"/api/admin/usage/heatmap",
"/api/admin/usage/records",
"/api/admin/usage/active",
"/api/admin/usage/cache-affinity/hit-analysis",
"/api/admin/usage/cache-affinity/interval-timeline",
"/api/admin/usage/cache-affinity/ttl-analysis",
"/api/admin/usage/{usage_id}/curl",
"/api/admin/usage/{usage_id}",
"/api/admin/usage/{usage_id}/replay",
"/api/admin/video-tasks",
"/api/admin/video-tasks/stats",
"/api/admin/video-tasks/{task_id}",
"/api/admin/video-tasks/{task_id}/cancel",
"/api/admin/video-tasks/{task_id}/video",
"/api/admin/wallets",
"/api/admin/wallets/ledger",
"/api/admin/wallets/refund-requests",
"/api/admin/wallets/{wallet_id}",
"/api/admin/wallets/{wallet_id}/transactions",
"/api/admin/wallets/{wallet_id}/refunds",
"/api/admin/wallets/{wallet_id}/adjust",
"/api/admin/wallets/{wallet_id}/recharge",
"/api/admin/wallets/{wallet_id}/refunds/{refund_id}/process",
"/api/admin/wallets/{wallet_id}/refunds/{refund_id}/complete",
"/api/admin/wallets/{wallet_id}/refunds/{refund_id}/fail",
"/api/admin/api-keys",
"/api/admin/api-keys/{key_id}",
"/api/admin/users",
"/api/admin/users/{user_id}",
"/api/admin/users/{user_id}/sessions",
"/api/admin/users/{user_id}/sessions/{session_id}",
"/api/admin/users/{user_id}/api-keys",
"/api/admin/users/{user_id}/api-keys/{key_id}",
"/api/admin/users/{user_id}/api-keys/{key_id}/lock",
"/api/admin/users/{user_id}/api-keys/{key_id}/full-key",
"/api/admin/pool/overview",
"/api/admin/pool/scheduling-presets",
"/api/admin/pool/{provider_id}/keys",
"/api/admin/pool/{provider_id}/keys/batch-delete-task/{task_id}",
"/api/admin/pool/{provider_id}/keys/batch-action",
"/api/admin/pool/{provider_id}/keys/batch-import",
"/api/admin/pool/{provider_id}/keys/cleanup-banned",
"/api/admin/pool/{provider_id}/keys/resolve-selection",
"/api/admin/proxy-nodes",
"/api/admin/proxy-nodes/register",
"/api/admin/proxy-nodes/heartbeat",
"/api/admin/proxy-nodes/unregister",
"/api/admin/proxy-nodes/manual",
"/api/admin/proxy-nodes/upgrade",
"/api/admin/proxy-nodes/test-url",
"/api/admin/proxy-nodes/{node_id}",
"/api/admin/proxy-nodes/{node_id}/test",
"/api/admin/proxy-nodes/{node_id}/config",
"/api/admin/proxy-nodes/{node_id}/events",
"/api/admin/models/catalog",
"/api/admin/models/external",
"/api/admin/models/external/cache",
"/api/admin/models/global",
"/api/admin/models/global/{global_model_id}",
"/api/admin/models/global/batch-delete",
"/api/admin/models/global/{global_model_id}/assign-to-providers",
"/api/admin/models/global/{global_model_id}/providers",
"/api/admin/models/global/{global_model_id}/routing",
}
def _route_paths(router: object) -> set[str]:
return {route.path for route in getattr(router, "routes", [])}
def _app_matches_http_route(path: str, method: str) -> bool:
scope = {
"type": "http",
"path": path,
"method": method,
"root_path": "",
}
return any(route.matches(scope)[0] is Match.FULL for route in main_module.app.routes)
def _router_matches_http_route(router: object, path: str, method: str) -> bool:
scope = {
"type": "http",
"path": path,
"method": method,
"root_path": "",
}
return any(route.matches(scope)[0] is Match.FULL for route in getattr(router, "routes", []))
def test_python_host_app_exposes_loopback_internal_gateway_bridge_routes() -> None:
host_route_paths = _route_paths(main_module.app)
legacy_bridge_paths = _route_paths(legacy_gateway_bridge_router)
assert "/api/internal/gateway/resolve" in legacy_bridge_paths
assert "/api/internal/gateway/auth-context" in legacy_bridge_paths
assert "/api/internal/gateway/decision-sync" in legacy_bridge_paths
assert "/api/internal/gateway/decision-stream" in legacy_bridge_paths
assert hasattr(internal_module, "legacy_gateway_bridge_router") is False
assert hasattr(internal_module, "LEGACY_GATEWAY_BRIDGE_PATH_PREFIXES") is False
assert not legacy_bridge_paths.issubset(host_route_paths)
assert not any(path.startswith(LEGACY_GATEWAY_BRIDGE_PATH_PREFIX) for path in host_route_paths)
def test_python_host_app_exposes_no_api_routes() -> None:
host_route_paths = _route_paths(main_module.app)
api_route_paths = sorted(path for path in host_route_paths if path.startswith("/api/"))
assert api_route_paths == []
def test_python_internal_router_excludes_gateway_bridge() -> None:
internal_route_paths = _route_paths(internal_module.python_internal_router)
assert not any(
path.startswith(LEGACY_GATEWAY_BRIDGE_PATH_PREFIX)
for path in internal_route_paths
)
assert not any(path.startswith("/api/internal/hub") for path in internal_route_paths)
def test_python_host_app_surface_keeps_shell_routes_and_rejects_removed_edges() -> None:
host_route_paths = _route_paths(main_module.app)
compat_route_paths = _route_paths(frontdoor_compat_router)
python_public_route_paths = _route_paths(python_public_router)
python_auth_route_paths = _route_paths(python_auth_router)
python_dashboard_route_paths = _route_paths(python_dashboard_router)
python_monitoring_route_paths = _route_paths(python_monitoring_router)
python_payment_route_paths = _route_paths(python_payment_router)
python_user_me_route_paths = _route_paths(python_user_me_router)
python_wallet_route_paths = _route_paths(python_wallet_router)
python_admin_route_paths = _route_paths(python_admin_router)
python_announcement_route_paths = _route_paths(python_announcement_router)
assert "/v1/chat/completions" in compat_route_paths
assert "/v1/messages" in compat_route_paths
assert "/v1beta/models/{model}:generateContent" in compat_route_paths
assert "/v1/videos" in compat_route_paths
assert "/v1beta/files" in compat_route_paths
assert "/v1/chat/completions" not in python_public_route_paths
assert "/v1/messages" not in python_public_route_paths
assert "/v1beta/models/{model}:generateContent" not in python_public_route_paths
assert "/v1/videos" not in python_public_route_paths
assert "/v1beta/files" not in python_public_route_paths
assert "/v1/models" not in python_public_route_paths
assert "/api/public/site-info" not in python_public_route_paths
assert "/api/public/providers" not in python_public_route_paths
assert "/api/public/models" not in python_public_route_paths
assert "/api/public/search/models" not in python_public_route_paths
assert "/api/public/stats" not in python_public_route_paths
assert "/api/public/global-models" not in python_public_route_paths
assert "/api/public/health/api-formats" not in python_public_route_paths
assert "/api/modules/auth-status" not in python_public_route_paths
assert "/api/capabilities" not in python_public_route_paths
assert "/api/capabilities/user-configurable" not in python_public_route_paths
assert "/api/capabilities/model/{model_name}" not in python_public_route_paths
assert "/api/auth/registration-settings" not in python_auth_route_paths
assert "/api/auth/settings" not in python_auth_route_paths
assert "/api/auth/login" not in python_auth_route_paths
assert "/api/auth/refresh" not in python_auth_route_paths
assert "/api/auth/register" not in python_auth_route_paths
assert "/api/auth/me" not in python_auth_route_paths
assert "/api/auth/logout" not in python_auth_route_paths
assert "/api/auth/send-verification-code" not in python_auth_route_paths
assert "/api/auth/verify-email" not in python_auth_route_paths
assert "/api/auth/verification-status" not in python_auth_route_paths
assert "/api/dashboard/stats" not in python_dashboard_route_paths
assert "/api/dashboard/recent-requests" not in python_dashboard_route_paths
assert "/api/dashboard/provider-status" not in python_dashboard_route_paths
assert "/api/dashboard/daily-stats" not in python_dashboard_route_paths
assert "/api/monitoring/my-audit-logs" not in python_monitoring_route_paths
assert "/api/monitoring/rate-limit-status" not in python_monitoring_route_paths
assert "/api/payment/callback/{payment_method}" not in python_payment_route_paths
assert "/api/wallet/balance" not in python_wallet_route_paths
assert "/api/wallet/transactions" not in python_wallet_route_paths
assert "/api/wallet/flow" not in python_wallet_route_paths
assert "/api/wallet/today-cost" not in python_wallet_route_paths
assert "/api/wallet/recharge" not in python_wallet_route_paths
assert "/api/wallet/recharge/{order_id}" not in python_wallet_route_paths
assert "/api/wallet/refunds" not in python_wallet_route_paths
assert "/api/wallet/refunds/{refund_id}" not in python_wallet_route_paths
assert "/api/users/me" not in python_user_me_route_paths
assert "/api/users/me/password" not in python_user_me_route_paths
assert "/api/users/me/sessions" not in python_user_me_route_paths
assert "/api/users/me/sessions/others" not in python_user_me_route_paths
assert "/api/users/me/sessions/{session_id}" not in python_user_me_route_paths
assert "/api/users/me/api-keys" not in python_user_me_route_paths
assert "/api/users/me/api-keys/{key_id}" not in python_user_me_route_paths
assert "/api/users/me/usage" not in python_user_me_route_paths
assert "/api/users/me/usage/active" not in python_user_me_route_paths
assert "/api/users/me/usage/interval-timeline" not in python_user_me_route_paths
assert "/api/users/me/usage/heatmap" not in python_user_me_route_paths
assert "/api/users/me/providers" not in python_user_me_route_paths
assert "/api/users/me/available-models" not in python_user_me_route_paths
assert "/api/users/me/endpoint-status" not in python_user_me_route_paths
assert "/api/users/me/api-keys/{api_key_id}/providers" not in python_user_me_route_paths
assert "/api/users/me/api-keys/{api_key_id}/capabilities" not in python_user_me_route_paths
assert "/api/users/me/preferences" not in python_user_me_route_paths
assert "/api/users/me/model-capabilities" not in python_user_me_route_paths
assert not (RUST_OWNED_ADMIN_PATHS & python_admin_route_paths)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements", "GET"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements/active", "GET"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements", "POST"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements/announcement-1", "PUT"
)
assert not _router_matches_http_route(
python_announcement_router, "/api/announcements/announcement-1", "DELETE"
)
assert not _router_matches_http_route(
python_announcement_router,
"/api/announcements/users/me/unread-count",
"GET",
)
assert not _router_matches_http_route(
python_announcement_router,
"/api/announcements/announcement-1/read-status",
"PATCH",
)
assert "/v1/chat/completions" not in host_route_paths
assert "/v1/messages" not in host_route_paths
assert "/v1beta/models/{model}:generateContent" not in host_route_paths
assert "/v1/videos" not in host_route_paths
assert "/v1beta/files" not in host_route_paths
assert "/v1/models" not in host_route_paths
assert "/v1/providers" not in host_route_paths
assert "/v1/test-connection" not in host_route_paths
assert "/api/public/site-info" not in host_route_paths
assert "/api/public/providers" not in host_route_paths
assert "/api/public/models" not in host_route_paths
assert "/api/public/search/models" not in host_route_paths
assert "/api/public/stats" not in host_route_paths
assert not _app_matches_http_route("/api/announcements", "GET")
assert not _app_matches_http_route("/api/announcements/active", "GET")
assert not _app_matches_http_route("/api/announcements", "POST")
assert not _app_matches_http_route("/api/announcements/announcement-1", "PUT")
assert not _app_matches_http_route("/api/announcements/announcement-1", "DELETE")
assert "/api/public/global-models" not in host_route_paths
assert "/api/public/health/api-formats" not in host_route_paths
assert "/api/modules/auth-status" not in host_route_paths
assert "/api/capabilities" not in host_route_paths
assert "/api/capabilities/user-configurable" not in host_route_paths
assert "/api/capabilities/model/{model_name}" not in host_route_paths
assert "/api/auth/registration-settings" not in host_route_paths
assert "/api/auth/settings" not in host_route_paths
assert "/api/auth/login" not in host_route_paths
assert "/api/auth/refresh" not in host_route_paths
assert "/api/auth/register" not in host_route_paths
assert "/api/auth/me" not in host_route_paths
assert "/api/auth/logout" not in host_route_paths
assert "/api/auth/send-verification-code" not in host_route_paths
assert "/api/auth/verify-email" not in host_route_paths
assert "/api/auth/verification-status" not in host_route_paths
assert "/api/dashboard/stats" not in host_route_paths
assert "/api/dashboard/recent-requests" not in host_route_paths
assert "/api/dashboard/provider-status" not in host_route_paths
assert "/api/dashboard/daily-stats" not in host_route_paths
assert "/api/monitoring/my-audit-logs" not in host_route_paths
assert "/api/monitoring/rate-limit-status" not in host_route_paths
assert "/api/payment/callback/{payment_method}" not in host_route_paths
assert "/api/wallet/balance" not in host_route_paths
assert "/api/wallet/transactions" not in host_route_paths
assert "/api/wallet/flow" not in host_route_paths
assert "/api/wallet/today-cost" not in host_route_paths
assert "/api/wallet/recharge" not in host_route_paths
assert "/api/wallet/recharge/{order_id}" not in host_route_paths
assert "/api/wallet/refunds" not in host_route_paths
assert "/api/wallet/refunds/{refund_id}" not in host_route_paths
assert not (RUST_OWNED_ADMIN_PATHS & host_route_paths)
assert "/health" not in host_route_paths
assert "/v1/health" not in host_route_paths
assert "/" not in host_route_paths
assert "/test-connection" not in host_route_paths
assert "/readyz" not in host_route_paths
assert _app_matches_http_route("/v1/chat/completions", "POST") is False
assert _app_matches_http_route("/v1/messages", "POST") is False
assert _app_matches_http_route("/v1beta/models/gemini-2.5-pro:generateContent", "POST") is False
assert _app_matches_http_route("/v1/videos", "POST") is False
assert _app_matches_http_route("/v1beta/files", "GET") is False
assert _app_matches_http_route("/v1/models", "GET") is False
assert _app_matches_http_route("/v1/providers", "GET") is False
assert _app_matches_http_route("/v1/test-connection", "GET") is False
assert _app_matches_http_route("/api/public/site-info", "GET") is False
assert _app_matches_http_route("/api/public/providers", "GET") is False
assert _app_matches_http_route("/api/public/models", "GET") is False
assert _app_matches_http_route("/api/public/search/models", "GET") is False
assert _app_matches_http_route("/api/public/stats", "GET") is False
assert _app_matches_http_route("/api/public/global-models", "GET") is False
assert _app_matches_http_route("/api/public/health/api-formats", "GET") is False
assert _app_matches_http_route("/api/modules/auth-status", "GET") is False
assert _app_matches_http_route("/api/capabilities", "GET") is False
assert _app_matches_http_route("/api/capabilities/user-configurable", "GET") is False
assert _app_matches_http_route("/api/capabilities/model/gpt-5", "GET") is False
assert _app_matches_http_route("/api/auth/registration-settings", "GET") is False
assert _app_matches_http_route("/api/auth/login", "POST") is False
assert _app_matches_http_route("/api/auth/refresh", "POST") is False
assert _app_matches_http_route("/api/auth/register", "POST") is False
assert _app_matches_http_route("/api/auth/me", "GET") is False
assert _app_matches_http_route("/api/auth/logout", "POST") is False
assert _app_matches_http_route("/api/auth/send-verification-code", "POST") is False
assert _app_matches_http_route("/api/auth/verify-email", "POST") is False
assert _app_matches_http_route("/api/auth/verification-status", "POST") is False
assert _app_matches_http_route("/api/admin/stats/comparison", "GET") is False
assert _app_matches_http_route("/api/admin/stats/errors/distribution", "GET") is False
assert _app_matches_http_route("/api/admin/stats/performance/percentiles", "GET") is False
assert _app_matches_http_route("/api/admin/stats/cost/forecast", "GET") is False
assert _app_matches_http_route("/api/admin/stats/time-series", "GET") is False
assert _app_matches_http_route("/api/admin/stats/leaderboard/users", "GET") is False
assert _app_matches_http_route("/api/auth/settings", "GET") is False
assert _app_matches_http_route("/api/dashboard/stats", "GET") is False
assert _app_matches_http_route("/api/dashboard/recent-requests", "GET") is False
assert _app_matches_http_route("/api/dashboard/provider-status", "GET") is False
assert _app_matches_http_route("/api/dashboard/daily-stats", "GET") is False
assert _app_matches_http_route("/api/monitoring/my-audit-logs", "GET") is False
assert _app_matches_http_route("/api/monitoring/rate-limit-status", "GET") is False
assert _app_matches_http_route("/api/payment/callback/alipay", "POST") is False
assert _app_matches_http_route("/api/wallet/balance", "GET") is False
assert _app_matches_http_route("/api/wallet/transactions", "GET") is False
assert _app_matches_http_route("/api/wallet/flow", "GET") is False
assert _app_matches_http_route("/api/wallet/today-cost", "GET") is False
assert _app_matches_http_route("/api/wallet/recharge", "GET") is False
assert _app_matches_http_route("/api/wallet/recharge", "POST") is False
assert _app_matches_http_route("/api/wallet/recharge/order-1", "GET") is False
assert _app_matches_http_route("/api/wallet/refunds", "GET") is False
assert _app_matches_http_route("/api/wallet/refunds", "POST") is False
assert _app_matches_http_route("/api/wallet/refunds/refund-1", "GET") is False
assert _app_matches_http_route("/api/users/me", "GET") is False
assert _app_matches_http_route("/api/users/me", "PUT") is False
assert _app_matches_http_route("/api/users/me/password", "PATCH") is False
assert _app_matches_http_route("/api/users/me/sessions", "GET") is False
assert _app_matches_http_route("/api/users/me/sessions/others", "DELETE") is False
assert _app_matches_http_route("/api/users/me/sessions/session-1", "PATCH") is False
assert _app_matches_http_route("/api/users/me/sessions/session-1", "DELETE") is False
assert _app_matches_http_route("/api/users/me/api-keys", "GET") is False
assert _app_matches_http_route("/api/users/me/api-keys", "POST") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "GET") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "DELETE") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "PUT") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1", "PATCH") is False
assert _app_matches_http_route("/api/users/me/usage", "GET") is False
assert _app_matches_http_route("/api/users/me/usage/active", "GET") is False
assert _app_matches_http_route("/api/users/me/usage/interval-timeline", "GET") is False
assert _app_matches_http_route("/api/users/me/usage/heatmap", "GET") is False
assert _app_matches_http_route("/api/users/me/providers", "GET") is False
assert _app_matches_http_route("/api/users/me/available-models", "GET") is False
assert _app_matches_http_route("/api/users/me/endpoint-status", "GET") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1/providers", "PUT") is False
assert _app_matches_http_route("/api/users/me/api-keys/key-1/capabilities", "PUT") is False
assert _app_matches_http_route("/api/users/me/preferences", "GET") is False
assert _app_matches_http_route("/api/users/me/preferences", "PUT") is False
assert _app_matches_http_route("/api/users/me/model-capabilities", "GET") is False
assert _app_matches_http_route("/api/users/me/model-capabilities", "PUT") is False
assert _app_matches_http_route("/api/announcements/announcement-1", "GET") is False
assert _app_matches_http_route("/api/announcements/users/me/unread-count", "GET") is False
assert (
_app_matches_http_route("/api/announcements/announcement-1/read-status", "PATCH")
is False
)
assert _app_matches_http_route("/api/admin/system/version", "GET") is False
assert _app_matches_http_route("/api/admin/system/settings", "GET") is False
assert _app_matches_http_route("/api/admin/system/config/export", "GET") is False
assert _app_matches_http_route("/api/admin/system/configs", "GET") is False
assert _app_matches_http_route("/api/admin/system/configs/smtp_password", "GET") is False
assert _app_matches_http_route("/api/admin/modules/status", "GET") is False
assert _app_matches_http_route("/api/admin/modules/status/auth", "GET") is False
assert _app_matches_http_route("/api/admin/modules/status/auth/enabled", "PUT") is False
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/connect",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/disconnect",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/actions/query_balance",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/provider-strategy/strategies", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/provider-strategy/providers/provider-openai/billing",
"PUT",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-strategy/providers/provider-openai/stats",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-strategy/providers/provider-openai/quota",
"DELETE",
)
is False
)
assert _app_matches_http_route("/api/admin/stats/providers/quota-usage", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/verify",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/balance",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/balance",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/checkin",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/provider-ops/batch/balance", "POST") is False
assert _app_matches_http_route("/api/admin/billing/presets", "GET") is False
assert _app_matches_http_route("/api/admin/billing/presets/apply", "POST") is False
assert _app_matches_http_route("/api/admin/billing/rules", "GET") is False
assert _app_matches_http_route("/api/admin/billing/rules/rule-1", "GET") is False
assert _app_matches_http_route("/api/admin/billing/rules", "POST") is False
assert _app_matches_http_route("/api/admin/billing/rules/rule-1", "PUT") is False
assert _app_matches_http_route("/api/admin/billing/collectors", "GET") is False
assert _app_matches_http_route("/api/admin/billing/collectors/collector-1", "GET") is False
assert _app_matches_http_route("/api/admin/billing/collectors", "POST") is False
assert _app_matches_http_route("/api/admin/billing/collectors/collector-1", "PUT") is False
assert _app_matches_http_route("/api/admin/provider-query/models", "POST") is False
assert _app_matches_http_route("/api/admin/provider-query/test-model", "POST") is False
assert (
_app_matches_http_route("/api/admin/provider-query/test-model-failover", "POST")
is False
)
assert _app_matches_http_route("/api/admin/payments/orders", "GET") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1", "GET") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1/expire", "POST") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1/credit", "POST") is False
assert _app_matches_http_route("/api/admin/payments/orders/order-1/fail", "POST") is False
assert _app_matches_http_route("/api/admin/payments/callbacks", "GET") is False
assert _app_matches_http_route("/api/admin/usage/aggregation/stats", "GET") is False
assert _app_matches_http_route("/api/admin/usage/stats", "GET") is False
assert _app_matches_http_route("/api/admin/usage/heatmap", "GET") is False
assert _app_matches_http_route("/api/admin/usage/records", "GET") is False
assert _app_matches_http_route("/api/admin/usage/active", "GET") is False
assert _app_matches_http_route("/api/admin/usage/usage-1/curl", "GET") is False
assert _app_matches_http_route("/api/admin/usage/usage-1", "GET") is False
assert _app_matches_http_route("/api/admin/usage/usage-1/replay", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes", "GET") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/register", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/heartbeat", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/unregister", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/manual", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/upgrade", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/test-url", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1", "PATCH") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1/test", "POST") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1/config", "PUT") is False
assert _app_matches_http_route("/api/admin/proxy-nodes/node-1/events", "GET") is False
assert _app_matches_http_route("/api/admin/wallets", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/ledger", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/refund-requests", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/wallet-1", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/wallet-1/transactions", "GET") is False
assert _app_matches_http_route("/api/admin/wallets/wallet-1/refunds", "GET") is False
assert _app_matches_http_route("/api/admin/api-keys", "GET") is False
assert _app_matches_http_route("/api/admin/api-keys", "POST") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "GET") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "PUT") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "PATCH") is False
assert _app_matches_http_route("/api/admin/api-keys/key-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users", "GET") is False
assert _app_matches_http_route("/api/admin/users", "POST") is False
assert _app_matches_http_route("/api/admin/users/user-1", "GET") is False
assert _app_matches_http_route("/api/admin/users/user-1", "PUT") is False
assert _app_matches_http_route("/api/admin/users/user-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/sessions", "GET") is False
assert _app_matches_http_route("/api/admin/users/user-1/sessions", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/sessions/session-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys", "GET") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys", "POST") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1", "DELETE") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1", "PUT") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1/lock", "PATCH") is False
assert _app_matches_http_route("/api/admin/users/user-1/api-keys/key-1/full-key", "GET") is False
assert _app_matches_http_route("/api/admin/system/email/templates", "GET") is False
assert _app_matches_http_route("/api/admin/system/email/templates/verification", "GET") is False
assert _app_matches_http_route("/api/admin/providers/", "GET") is False
assert _app_matches_http_route("/api/admin/providers/", "POST") is False
assert _app_matches_http_route("/api/admin/providers/provider-openai", "PATCH") is False
assert _app_matches_http_route("/api/admin/providers/provider-openai/summary", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/health-monitor",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/mapping-preview",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/delete-task/task-1",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/pool-status",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/models/model-1",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/models/batch",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/assign-global-models",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/providers/provider-openai/import-from-upstream",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/endpoints/providers/provider-openai/endpoints",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/endpoints/defaults/openai:responses/body-rules",
"GET",
)
is False
)
assert _app_matches_http_route("/api/admin/endpoints/endpoint-1", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/keys/key-1/export", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/keys/key-1/reveal", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/endpoints/providers/provider-openai/keys",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/endpoints/providers/provider-openai/refresh-quota",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/endpoints/keys/batch-delete", "POST") is False
assert _app_matches_http_route("/api/admin/endpoints/rpm/key/key-1", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/health/status", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/health/key/key-1", "GET") is False
assert _app_matches_http_route("/api/admin/endpoints/health/keys/key-1", "PATCH") is False
assert _app_matches_http_route("/api/admin/provider-oauth/supported-types", "GET") is False
assert _app_matches_http_route("/api/admin/provider-oauth/keys/key-1/start", "POST") is False
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-kiro/device-authorize",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-kiro/device-poll",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/import-refresh-token",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/batch-import",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/batch-import/tasks",
"POST",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-oauth/providers/provider-codex/batch-import/tasks/task-1",
"GET",
)
is False
)
assert _app_matches_http_route("/api/admin/provider-ops/architectures", "GET") is False
assert (
_app_matches_http_route(
"/api/admin/provider-ops/architectures/generic_api",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/status",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/config",
"GET",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/config",
"PUT",
)
is False
)
assert (
_app_matches_http_route(
"/api/admin/provider-ops/providers/provider-openai/config",
"DELETE",
)
is False
)
assert _app_matches_http_route("/api/admin/video-tasks", "GET") is False
assert _app_matches_http_route("/api/admin/video-tasks/stats", "GET") is False
assert _app_matches_http_route("/api/admin/video-tasks/task-1", "GET") is False
assert _app_matches_http_route("/api/admin/video-tasks/task-1/cancel", "POST") is False
assert _app_matches_http_route("/api/admin/video-tasks/task-1/video", "GET") is False
assert _app_matches_http_route("/api/admin/adaptive/keys", "GET") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/mode", "PATCH") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/stats", "GET") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/learning", "DELETE") is False
assert _app_matches_http_route("/api/admin/adaptive/keys/key-1/limit", "PATCH") is False
assert _app_matches_http_route("/api/admin/adaptive/summary", "GET") is False
assert _app_matches_http_route("/api/admin/models/catalog", "GET") is False
assert _app_matches_http_route("/api/admin/models/external", "GET") is False
assert _app_matches_http_route("/api/admin/models/external/cache", "DELETE") is False
assert _app_matches_http_route("/api/admin/models/global", "GET") is False
assert _app_matches_http_route("/api/admin/models/global", "POST") is False
assert _app_matches_http_route("/api/admin/models/global/test-id", "GET") is False
assert _app_matches_http_route("/api/admin/models/global/test-id", "PATCH") is False
assert _app_matches_http_route("/api/admin/models/global/test-id", "DELETE") is False
assert _app_matches_http_route("/api/admin/models/global/batch-delete", "POST") is False
assert (
_app_matches_http_route(
"/api/admin/models/global/test-id/assign-to-providers",
"POST",
)
is False
)
assert _app_matches_http_route("/api/admin/models/global/test-id/providers", "GET") is False
assert _app_matches_http_route("/api/admin/models/global/test-id/routing", "GET") is False
assert _app_matches_http_route("/health", "GET") is False
assert _app_matches_http_route("/v1/health", "GET") is False
assert _app_matches_http_route("/", "GET") is False
assert _app_matches_http_route("/test-connection", "GET") is False
assert _app_matches_http_route("/api/internal/gateway/auth-context", "POST") is False
assert _app_matches_http_route("/api/internal/gateway/resolve", "POST") is False
assert _app_matches_http_route("/api/internal/gateway/decision-sync", "POST") is False
assert _app_matches_http_route("/api/internal/hub/heartbeat", "POST") is False
assert _app_matches_http_route("/api/internal/hub/node-status", "POST") is False
assert _app_matches_http_route("/readyz", "GET") is False
tags = {tag.get("name") for tag in main_module.app.openapi().get("tags", [])}
assert "OpenAI API" not in tags
assert "Claude API" not in tags
assert "Gemini API" not in tags
assert "Gemini Files API" not in tags
assert "System Catalog" not in tags
def test_python_payment_host_surface_is_single_dynamic_callback_route() -> None:
host_route_paths = _route_paths(main_module.app)
assert "/api/payment/callback/{payment_method}" not in host_route_paths
assert "/api/payment/callback/alipay" not in host_route_paths
assert "/api/payment/callback/wechat" not in host_route_paths
assert _app_matches_http_route("/api/payment/callback/alipay", "POST") is False
assert _app_matches_http_route("/api/payment/callback/wechat", "POST") is False

View File

@@ -6,6 +6,7 @@ from urllib.parse import parse_qs, urlparse
import httpx
import pytest
from src.services.auth.oauth.models import OAuthFlowError
from src.services.auth.oauth.providers.linuxdo import LinuxDoOAuthProvider
@@ -33,6 +34,28 @@ def test_linuxdo_authorization_url_omits_empty_scope() -> None:
assert params["state"] == ["state-1"]
@pytest.mark.asyncio
async def test_linuxdo_exchange_code_requires_rust_executor() -> None:
provider = LinuxDoOAuthProvider()
with pytest.raises(OAuthFlowError) as exc_info:
await provider.exchange_code(_make_config(), "code-0")
assert exc_info.value.error_code == "provider_unavailable"
assert exc_info.value.detail == "OAuth 仅支持 Rust executor"
@pytest.mark.asyncio
async def test_linuxdo_get_user_info_requires_rust_executor() -> None:
provider = LinuxDoOAuthProvider()
with pytest.raises(OAuthFlowError) as exc_info:
await provider.get_user_info(_make_config(), "access-token")
assert exc_info.value.error_code == "provider_unavailable"
assert exc_info.value.detail == "OAuth 仅支持 Rust executor"
@pytest.mark.asyncio
async def test_linuxdo_exchange_code_uses_basic_auth(monkeypatch: pytest.MonkeyPatch) -> None:
provider = LinuxDoOAuthProvider()

View File

@@ -11,6 +11,7 @@ from sqlalchemy.orm import sessionmaker
from src.core.enums import AuthSource, UserRole
from src.models.database import Base, OAuthProvider, User, UserOAuthLink
from src.services.auth.oauth.models import OAuthFlowError
from src.services.auth.oauth.service import OAuthService
from src.services.auth.oauth.state import OAuthStateData
@@ -133,6 +134,127 @@ async def test_handle_callback_allows_bind_state_without_device_id(
assert parse_qs(parsed.query)["oauth_bound"] == ["GitHub"]
@pytest.mark.asyncio
async def test_handle_callback_redirects_when_provider_http_is_rust_only(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
provider = SimpleNamespace(
exchange_code=AsyncMock(
side_effect=OAuthFlowError("provider_unavailable", "OAuth 仅支持 Rust executor")
),
get_user_info=AsyncMock(),
)
config = SimpleNamespace(
frontend_callback_url="https://app.example.com/auth/callback",
display_name="GitHub",
is_enabled=True,
)
monkeypatch.setattr(
"src.services.auth.oauth.service.OAuthService._require_module_active",
lambda _db: None,
)
monkeypatch.setattr(
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
lambda _provider_type: provider,
)
monkeypatch.setattr(
"src.services.auth.oauth.service.OAuthService._get_provider_config",
lambda _db, _provider_type: config,
)
monkeypatch.setattr(
"src.services.auth.oauth.service.get_redis_client",
AsyncMock(return_value=object()),
)
monkeypatch.setattr(
"src.services.auth.oauth.service.consume_oauth_state",
AsyncMock(
return_value=OAuthStateData(
nonce="state-1",
provider_type="github",
action="login",
user_id=None,
client_device_id="device-1",
created_at=123,
)
),
)
result = await OAuthService.handle_callback(
db=db,
provider_type="github",
state="state-1",
code="code-1",
error=None,
error_description=None,
client_ip=None,
user_agent="pytest-agent",
headers={},
)
parsed = urlparse(result.redirect_url)
params = parse_qs(parsed.query)
assert params["error_code"] == ["provider_unavailable"]
assert params["error_detail"] == ["OAuth 仅支持 Rust executor"]
provider.get_user_info.assert_not_called()
@pytest.mark.asyncio
async def test_test_provider_config_returns_rust_only_failure_without_network(
monkeypatch: pytest.MonkeyPatch,
) -> None:
db = MagicMock()
monkeypatch.setattr(
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
lambda _provider_type: SimpleNamespace(),
)
monkeypatch.setattr(
"src.services.auth.oauth.service.OAuthService._get_provider_config",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("Python OAuth config probe should not read DB config")
),
)
result = await OAuthService.test_provider_config(db, "github")
assert result == {
"authorization_url_reachable": False,
"token_url_reachable": False,
"secret_status": "unsupported",
"details": "OAuth 配置测试仅支持 Rust executor",
}
@pytest.mark.asyncio
async def test_test_provider_config_with_data_returns_rust_only_failure(
monkeypatch: pytest.MonkeyPatch,
) -> None:
provider = SimpleNamespace(
authorization_url="https://provider.example/authorize",
token_url="https://provider.example/token",
)
monkeypatch.setattr(
"src.services.auth.oauth.service.OAuthService._get_provider_impl",
lambda _provider_type: provider,
)
result = await OAuthService.test_provider_config_with_data(
provider_type="github",
client_id="client-id",
client_secret="secret",
authorization_url_override=None,
token_url_override=None,
redirect_uri="https://api.example.com/api/oauth/github/callback",
)
assert result == {
"authorization_url_reachable": False,
"token_url_reachable": False,
"secret_status": "unsupported",
"details": "OAuth 配置测试仅支持 Rust executor",
}
def test_handle_login_sync_returns_user_snapshot_outside_db_session(
monkeypatch: pytest.MonkeyPatch,
) -> None:

View File

@@ -173,6 +173,49 @@ def test_persist_refreshed_token_preserves_true_account_block(
assert key.oauth_invalid_reason == "[ACCOUNT_BLOCK] Google requires verification"
def test_mark_oauth_token_expired_preserves_existing_account_block() -> None:
key = SimpleNamespace(
id="key-1",
oauth_invalid_at="old-invalid-at",
oauth_invalid_reason="[ACCOUNT_BLOCK] Google requires verification",
)
module._mark_oauth_token_expired(key, expires_at=1_900_000_000)
assert key.oauth_invalid_at == "old-invalid-at"
assert key.oauth_invalid_reason == "[ACCOUNT_BLOCK] Google requires verification"
def test_mark_oauth_token_expired_marks_detached_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
key = SimpleNamespace(id="key-1", oauth_invalid_at=None, oauth_invalid_reason=None)
row = SimpleNamespace(id="key-1", oauth_invalid_at=None, oauth_invalid_reason=None)
fake_db = _FakeDB(row)
monkeypatch.setattr(
module, "object_session", lambda _key: (_ for _ in ()).throw(RuntimeError())
)
_install_module(
monkeypatch,
"src.database",
{"create_session": lambda: _FakeSessionCtx(fake_db)},
)
_install_module(
monkeypatch,
"src.models.database",
{"ProviderAPIKey": type("ProviderAPIKey", (), {"id": "id"})},
)
module._mark_oauth_token_expired(key, expires_at=1_900_000_000)
assert fake_db.committed is True
assert key.oauth_invalid_at is not None
assert row.oauth_invalid_at is not None
assert str(key.oauth_invalid_reason).startswith("[OAUTH_EXPIRED] Token 已过期且续期失败")
assert "expired_at=1900000000" in str(row.oauth_invalid_reason)
@pytest.mark.asyncio
async def test_refresh_generic_oauth_token_persists_enriched_account_name(
monkeypatch: pytest.MonkeyPatch,

View File

@@ -252,6 +252,54 @@ def test_clear_oauth_invalid_response_invalidates_caches(
assert cache_calls == [("key", "key-1"), ("models", None)]
def test_clear_oauth_invalid_response_noops_without_invalid_marker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
cache_calls: list[tuple[str, str | None]] = []
async def _fake_invalidate_key_cache(key_id: str) -> None:
cache_calls.append(("key", key_id))
async def _fake_invalidate_models_cache() -> None:
cache_calls.append(("models", None))
fake_provider_cache_module = types.ModuleType("src.services.cache.provider_cache")
class _FakeProviderCacheService:
@staticmethod
async def invalidate_provider_api_key_cache(key_id: str) -> None:
await _fake_invalidate_key_cache(key_id)
setattr(fake_provider_cache_module, "ProviderCacheService", _FakeProviderCacheService)
monkeypatch.setitem(
sys.modules, "src.services.cache.provider_cache", fake_provider_cache_module
)
fake_models_service_module = types.ModuleType("src.services.cache.model_list_cache")
setattr(
fake_models_service_module, "invalidate_models_list_cache", _fake_invalidate_models_cache
)
monkeypatch.setitem(
sys.modules, "src.services.cache.model_list_cache", fake_models_service_module
)
key = SimpleNamespace(
oauth_invalid_at=None,
oauth_invalid_reason=None,
is_active=False,
)
db = _FakeClearOAuthDB(key=key)
result = command_module.clear_oauth_invalid_response(cast(Any, db), key_id="key-1")
assert result["message"] == "该 Key 当前无失效标记,无需清除"
assert key.oauth_invalid_at is None
assert key.oauth_invalid_reason is None
assert key.is_active is False
assert db.commit_count == 0
assert cache_calls == []
@pytest.mark.asyncio
async def test_run_delete_key_side_effects_skip_disassociate(
monkeypatch: pytest.MonkeyPatch,

View File

@@ -89,6 +89,18 @@ def _install_module(monkeypatch: pytest.MonkeyPatch, name: str, attrs: dict[str,
monkeypatch.setitem(sys.modules, name, module)
def _patch_rust_codex_response(
monkeypatch: pytest.MonkeyPatch,
module: Any,
response: Any | None,
) -> None:
monkeypatch.setattr(
module,
"_try_rust_codex_quota_response",
AsyncMock(return_value=response),
)
@pytest.mark.asyncio
async def test_codex_refresher_endpoint_missing_returns_error() -> None:
key = SimpleNamespace(id="k1", name="K1")
@@ -128,15 +140,12 @@ async def test_codex_refresher_http_non_200_returns_error(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
monkeypatch.setattr(module.crypto_service, "decrypt", lambda _v: "sk-test")
response = _FakeResponse(status_code=503, payload={"x": 1})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -241,15 +250,12 @@ async def test_codex_refresher_http_401_marks_auth_invalid_without_disabling_key
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
monkeypatch.setattr(module.crypto_service, "decrypt", lambda _v: "sk-test")
response = _FakeResponse(status_code=401, payload={"error": {"message": "token expired"}})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -295,7 +301,6 @@ async def test_codex_refresher_http_402_sets_quota_exhausted_metadata(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -309,9 +314,7 @@ async def test_codex_refresher_http_402_sets_quota_exhausted_metadata(
),
)
response = _FakeResponse(status_code=402, payload={"error": {"message": "payment required"}})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -362,7 +365,6 @@ async def test_codex_refresher_success_preserves_refresh_failed_marker(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -379,9 +381,7 @@ async def test_codex_refresher_success_preserves_refresh_failed_marker(
module, "parse_codex_wham_usage_response", lambda _data: {"used_percent": 10.0}
)
response = _FakeResponse(status_code=200, payload={"ok": True})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -430,7 +430,6 @@ async def test_codex_refresher_quota_exhausted_preserves_refresh_failed_marker(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -444,9 +443,7 @@ async def test_codex_refresher_quota_exhausted_preserves_refresh_failed_marker(
),
)
response = _FakeResponse(status_code=402, payload={"error": {"message": "payment required"}})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -490,7 +487,6 @@ async def test_codex_refresher_http_403_token_invalidated_marks_oauth_expired(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -499,9 +495,7 @@ async def test_codex_refresher_http_403_token_invalidated_marks_oauth_expired(
status_code=403,
payload={"error": {"message": "Authentication token has been invalidated."}},
)
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -541,7 +535,6 @@ async def test_codex_refresher_http_403_generic_marks_soft_request_failed(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -550,9 +543,7 @@ async def test_codex_refresher_http_403_generic_marks_soft_request_failed(
status_code=403,
payload={"error": {"message": "Access forbidden for this account."}},
)
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -599,7 +590,6 @@ async def test_codex_refresher_success_updates_metadata(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -608,9 +598,7 @@ async def test_codex_refresher_success_updates_metadata(
module, "parse_codex_wham_usage_response", lambda _data: {"used_percent": 10.0}
)
response = _FakeResponse(status_code=200, payload={"ok": True})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -647,7 +635,6 @@ async def test_codex_refresher_parse_error_is_diagnostic(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -660,9 +647,7 @@ async def test_codex_refresher_parse_error_is_diagnostic(
),
)
response = _FakeResponse(status_code=200, payload={"ok": True})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -706,7 +691,6 @@ async def test_codex_refresher_oauth_missing_plan_type_adds_account_header(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -717,12 +701,11 @@ async def test_codex_refresher_oauth_missing_plan_type_adds_account_header(
module.crypto_service, "decrypt", lambda _v: json.dumps({"account_id": "acc-1"})
)
def _client_factory(**kwargs: Any) -> _FakeAsyncClient:
client = _FakeAsyncClient(response, **kwargs)
client_ref["client"] = client
return client
async def _fake_rust_response(**kwargs: Any) -> Any:
client_ref["client"] = SimpleNamespace(last_headers=dict(kwargs["headers"]))
return response
monkeypatch.setattr(module.httpx, "AsyncClient", _client_factory)
monkeypatch.setattr(module, "_try_rust_codex_quota_response", _fake_rust_response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -766,7 +749,6 @@ async def test_codex_refresher_oauth_uppercase_free_does_not_add_account_header(
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -779,12 +761,11 @@ async def test_codex_refresher_oauth_uppercase_free_does_not_add_account_header(
lambda _v: json.dumps({"account_id": "acc-1", "plan_type": "FREE"}),
)
def _client_factory(**kwargs: Any) -> _FakeAsyncClient:
client = _FakeAsyncClient(response, **kwargs)
client_ref["client"] = client
return client
async def _fake_rust_response(**kwargs: Any) -> Any:
client_ref["client"] = SimpleNamespace(last_headers=dict(kwargs["headers"]))
return response
monkeypatch.setattr(module.httpx, "AsyncClient", _client_factory)
monkeypatch.setattr(module, "_try_rust_codex_quota_response", _fake_rust_response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),
@@ -1216,7 +1197,6 @@ async def test_codex_refresher_http_402_workspace_deactivated_marks_account_bloc
"src.services.proxy_node.resolver",
{
"resolve_effective_proxy": lambda provider_proxy, key_proxy: None,
"build_proxy_client_kwargs": lambda proxy, timeout: {"timeout": timeout},
},
)
monkeypatch.setattr(module, "get_provider_auth", _fake_auth_info)
@@ -1230,9 +1210,7 @@ async def test_codex_refresher_http_402_workspace_deactivated_marks_account_bloc
),
)
response = _FakeResponse(status_code=402, payload={"detail": {"code": "deactivated_workspace"}})
monkeypatch.setattr(
module.httpx, "AsyncClient", lambda **kwargs: _FakeAsyncClient(response, **kwargs)
)
_patch_rust_codex_response(monkeypatch, module, response)
result = await refresh_codex_key_quota(
db=cast(Any, _FakeDB()),

View File

@@ -1,12 +1,19 @@
from __future__ import annotations
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock
import pytest
from src.config import config as runtime_config
from src.services.provider_ops.service import ProviderOpsService
from src.services.provider_ops.types import ConnectorAuthType, ProviderActionType
from src.services.provider_ops.types import (
ActionStatus,
ConnectorAuthType,
ProviderActionType,
ProviderOpsConfig,
)
from src.services.request.rust_executor_client import RustExecutorSyncResult
@@ -53,6 +60,14 @@ class _FakeRegistry:
return self._architecture
class _ExplodingConnector:
async def is_authenticated(self) -> bool:
raise AssertionError("Python connector auth check should not run")
def get_client(self) -> Any:
raise AssertionError("Python connector client should not be created")
class _SuccessResult:
def __init__(self) -> None:
self.success = True
@@ -165,3 +180,106 @@ async def test_verify_auth_prefers_rust_executor(
assert captured["plan"].proxy.mode == "tunnel"
assert captured["plan"].proxy.node_id == "node-1"
cache_balance.assert_awaited_once_with("provider-1", 2.0, {"window": "day"})
@pytest.mark.asyncio
async def test_verify_auth_returns_explicit_failure_when_rust_verifier_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
service = ProviderOpsService(_FakeDB())
architecture = _SuccessArchitecture()
monkeypatch.setattr(
"src.services.provider_ops.service.get_registry",
lambda: _FakeRegistry(architecture),
)
monkeypatch.setattr(
service,
"_try_rust_verify_response",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.proxy_node.resolver.resolve_ops_proxy_config_async",
AsyncMock(return_value=(None, None)),
)
result = await service.verify_auth(
base_url="https://example.com",
architecture_id="sub2api",
auth_type=ConnectorAuthType.SESSION_LOGIN,
config={},
credentials={"access_token": "token"},
)
assert result == {"success": False, "message": "认证验证仅支持 Rust executor"}
@pytest.mark.asyncio
@pytest.mark.parametrize("backend", ["python", "rust"])
async def test_connect_returns_explicit_failure_without_python_connector(
monkeypatch: pytest.MonkeyPatch,
backend: str,
) -> None:
service = ProviderOpsService(_FakeDB())
service._connectors["provider-1"] = object() # type: ignore[assignment]
monkeypatch.setattr(runtime_config, "executor_backend", backend)
monkeypatch.setattr(
service,
"_get_provider",
lambda _provider_id: SimpleNamespace(base_url="https://example.com"),
)
monkeypatch.setattr(
service,
"get_config",
lambda _provider_id: ProviderOpsConfig(
architecture_id="sub2api",
base_url="https://example.com",
connector_auth_type=ConnectorAuthType.API_KEY,
),
)
monkeypatch.setattr(
"src.services.provider_ops.service.get_registry",
lambda: (_ for _ in ()).throw(
AssertionError("Python provider connector registry should not be used")
),
)
success, message = await service.connect("provider-1", {"api_key": "token"})
assert success is False
assert message == "Provider 连接仅支持 Rust executor"
assert "provider-1" not in service._connectors
@pytest.mark.asyncio
@pytest.mark.parametrize("backend", ["python", "rust"])
async def test_execute_action_returns_not_supported_without_python_connector(
monkeypatch: pytest.MonkeyPatch,
backend: str,
) -> None:
service = ProviderOpsService(_FakeDB())
service._connectors["provider-1"] = _ExplodingConnector() # type: ignore[assignment]
monkeypatch.setattr(runtime_config, "executor_backend", backend)
monkeypatch.setattr(
service,
"get_config",
lambda _provider_id: ProviderOpsConfig(
architecture_id="sub2api",
base_url="https://example.com",
connector_auth_type=ConnectorAuthType.API_KEY,
),
)
monkeypatch.setattr(
"src.services.provider_ops.service.get_registry",
lambda: (_ for _ in ()).throw(
AssertionError("Python provider action architecture should not be used")
),
)
result = await service.execute_action("provider-1", ProviderActionType.QUERY_BALANCE)
assert result.status == ActionStatus.NOT_SUPPORTED
assert result.action_type == ProviderActionType.QUERY_BALANCE
assert result.message == "Provider 操作仅支持 Rust executor"

View File

@@ -79,6 +79,22 @@ def test_codex_openai_cli_does_not_duplicate_responses_suffix() -> None:
assert url == "https://chatgpt.com/backend-api/codex/responses"
def test_codex_openai_cli_supports_backendapi_variant() -> None:
endpoint = _DummyEndpoint(
base_url="https://chatgpt.com/backendapi/codex",
api_format="openai:cli",
provider=SimpleNamespace(provider_type="codex"),
)
url = build_provider_url(
endpoint, # type: ignore[arg-type]
path_params={"model": "ignored"},
is_stream=True,
)
assert url == "https://chatgpt.com/backendapi/codex/responses"
def test_codex_openai_cli_uses_compact_suffix_when_context_marked_compact() -> None:
endpoint = _DummyEndpoint(
base_url="https://chatgpt.com/backend-api/codex",

View File

@@ -6,7 +6,7 @@ from collections.abc import Callable
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from typing import Any, cast
from unittest.mock import MagicMock
from unittest.mock import AsyncMock, MagicMock
import pytest
@@ -49,6 +49,34 @@ async def test_maintenance_scheduler_start_skips_startup_task_when_disabled(
assert created is False
@pytest.mark.asyncio
async def test_maintenance_scheduler_start_skips_startup_task_when_no_python_owner(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(config, "maintenance_startup_tasks_enabled", True)
monkeypatch.setattr(config, "pending_cleanup_python_enabled", False)
monkeypatch.setattr(config, "antigravity_ua_refresh_python_enabled", False)
scheduler = MaintenanceScheduler()
safe_create_task = MagicMock()
monkeypatch.setattr("src.utils.async_utils.safe_create_task", safe_create_task)
monkeypatch.setattr(scheduler, "_get_checkin_time", lambda: (1, 5))
monkeypatch.setattr(
maintenance_scheduler_module,
"get_scheduler",
lambda: SimpleNamespace(
add_cron_job=lambda *args, **kwargs: None,
add_interval_job=lambda *args, **kwargs: None,
),
)
await scheduler.start()
safe_create_task.assert_not_called()
assert scheduler._startup_task is None
@pytest.mark.asyncio
async def test_maintenance_scheduler_stop_cancels_startup_task_and_removes_jobs(
monkeypatch: pytest.MonkeyPatch,
@@ -89,6 +117,518 @@ async def test_maintenance_scheduler_stop_cancels_startup_task_and_removes_jobs(
assert scheduler._registered_job_ids == []
@pytest.mark.asyncio
async def test_maintenance_scheduler_start_skips_rust_owned_maintenance_jobs_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(config, "maintenance_startup_tasks_enabled", False)
monkeypatch.setattr(config, "audit_cleanup_python_enabled", False)
monkeypatch.setattr(config, "db_maintenance_python_enabled", False)
monkeypatch.setattr(config, "gemini_file_mapping_cleanup_python_enabled", False)
monkeypatch.setattr(config, "http_client_idle_cleanup_python_enabled", False)
monkeypatch.setattr(config, "pending_cleanup_python_enabled", False)
monkeypatch.setattr(config, "pool_monitor_python_enabled", False)
monkeypatch.setattr(config, "provider_checkin_python_enabled", False)
monkeypatch.setattr(config, "request_candidate_cleanup_python_enabled", False)
monkeypatch.setattr(config, "stats_aggregation_python_enabled", False)
monkeypatch.setattr(config, "stats_hourly_aggregation_python_enabled", False)
monkeypatch.setattr(config, "antigravity_ua_refresh_python_enabled", False)
monkeypatch.setattr(config, "usage_cleanup_python_enabled", False)
monkeypatch.setattr(config, "wallet_daily_usage_aggregation_python_enabled", False)
scheduler = MaintenanceScheduler()
cron_job_ids: list[str] = []
interval_job_ids: list[str] = []
monkeypatch.setattr(scheduler, "_get_checkin_time", lambda: (1, 5))
monkeypatch.setattr(
maintenance_scheduler_module,
"get_scheduler",
lambda: SimpleNamespace(
add_cron_job=lambda *, job_id, **kwargs: cron_job_ids.append(job_id),
add_interval_job=lambda *, job_id, **kwargs: interval_job_ids.append(job_id),
),
)
await scheduler.start()
assert "stats_aggregation" not in cron_job_ids
assert "audit_cleanup" not in cron_job_ids
assert "candidate_cleanup" not in cron_job_ids
assert "db_maintenance" not in cron_job_ids
assert "stats_hourly_aggregation" not in cron_job_ids
assert "pool_monitor" not in interval_job_ids
assert "http_client_idle_cleanup" not in interval_job_ids
assert "usage_cleanup" not in cron_job_ids
assert "wallet_daily_usage_aggregation" not in cron_job_ids
assert scheduler.CHECKIN_JOB_ID not in cron_job_ids
assert "antigravity_ua_refresh" not in interval_job_ids
assert "gemini_file_mapping_cleanup" not in interval_job_ids
assert "pending_cleanup" not in interval_job_ids
assert "stats_aggregation" not in scheduler._registered_job_ids
assert "audit_cleanup" not in scheduler._registered_job_ids
assert "candidate_cleanup" not in scheduler._registered_job_ids
assert "db_maintenance" not in scheduler._registered_job_ids
assert "stats_hourly_aggregation" not in scheduler._registered_job_ids
assert "pool_monitor" not in scheduler._registered_job_ids
assert "http_client_idle_cleanup" not in scheduler._registered_job_ids
assert "usage_cleanup" not in scheduler._registered_job_ids
assert "wallet_daily_usage_aggregation" not in scheduler._registered_job_ids
assert scheduler.CHECKIN_JOB_ID not in scheduler._registered_job_ids
assert "antigravity_ua_refresh" not in scheduler._registered_job_ids
assert "gemini_file_mapping_cleanup" not in scheduler._registered_job_ids
assert "pending_cleanup" not in scheduler._registered_job_ids
@pytest.mark.asyncio
async def test_maintenance_scheduler_start_registers_python_owned_maintenance_jobs_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(config, "maintenance_startup_tasks_enabled", False)
monkeypatch.setattr(config, "audit_cleanup_python_enabled", True)
monkeypatch.setattr(config, "db_maintenance_python_enabled", True)
monkeypatch.setattr(config, "gemini_file_mapping_cleanup_python_enabled", True)
monkeypatch.setattr(config, "http_client_idle_cleanup_python_enabled", True)
monkeypatch.setattr(config, "pending_cleanup_python_enabled", True)
monkeypatch.setattr(config, "pool_monitor_python_enabled", True)
monkeypatch.setattr(config, "provider_checkin_python_enabled", True)
monkeypatch.setattr(config, "request_candidate_cleanup_python_enabled", True)
monkeypatch.setattr(config, "stats_aggregation_python_enabled", True)
monkeypatch.setattr(config, "stats_hourly_aggregation_python_enabled", True)
monkeypatch.setattr(config, "antigravity_ua_refresh_python_enabled", True)
monkeypatch.setattr(config, "usage_cleanup_python_enabled", True)
monkeypatch.setattr(config, "wallet_daily_usage_aggregation_python_enabled", True)
scheduler = MaintenanceScheduler()
cron_job_ids: list[str] = []
interval_job_ids: list[str] = []
monkeypatch.setattr(scheduler, "_get_checkin_time", lambda: (1, 5))
monkeypatch.setattr(
maintenance_scheduler_module,
"get_scheduler",
lambda: SimpleNamespace(
add_cron_job=lambda *, job_id, **kwargs: cron_job_ids.append(job_id),
add_interval_job=lambda *, job_id, **kwargs: interval_job_ids.append(job_id),
),
)
await scheduler.start()
assert "stats_aggregation" in cron_job_ids
assert "audit_cleanup" in cron_job_ids
assert "candidate_cleanup" in cron_job_ids
assert "db_maintenance" in cron_job_ids
assert "stats_hourly_aggregation" in cron_job_ids
assert "pool_monitor" in interval_job_ids
assert "http_client_idle_cleanup" in interval_job_ids
assert "usage_cleanup" in cron_job_ids
assert "wallet_daily_usage_aggregation" in cron_job_ids
assert scheduler.CHECKIN_JOB_ID in cron_job_ids
assert "antigravity_ua_refresh" in interval_job_ids
assert "gemini_file_mapping_cleanup" in interval_job_ids
assert "pending_cleanup" in interval_job_ids
assert "stats_aggregation" in scheduler._registered_job_ids
assert "audit_cleanup" in scheduler._registered_job_ids
assert "candidate_cleanup" in scheduler._registered_job_ids
assert "db_maintenance" in scheduler._registered_job_ids
assert "stats_hourly_aggregation" in scheduler._registered_job_ids
assert "pool_monitor" in scheduler._registered_job_ids
assert "http_client_idle_cleanup" in scheduler._registered_job_ids
assert "usage_cleanup" in scheduler._registered_job_ids
assert "wallet_daily_usage_aggregation" in scheduler._registered_job_ids
assert scheduler.CHECKIN_JOB_ID in scheduler._registered_job_ids
assert "antigravity_ua_refresh" in scheduler._registered_job_ids
assert "gemini_file_mapping_cleanup" in scheduler._registered_job_ids
assert "pending_cleanup" in scheduler._registered_job_ids
@pytest.mark.asyncio
async def test_maintenance_scheduler_startup_tasks_skip_python_pending_cleanup_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scheduler = MaintenanceScheduler()
refresh_user_agent = AsyncMock()
perform_pending_cleanup = AsyncMock()
monkeypatch.setattr(config, "pending_cleanup_python_enabled", False)
monkeypatch.setattr(config, "antigravity_ua_refresh_python_enabled", False)
monkeypatch.setattr(maintenance_scheduler_module.asyncio, "sleep", AsyncMock())
monkeypatch.setattr(
"src.services.provider.adapters.antigravity.client.refresh_user_agent",
refresh_user_agent,
)
monkeypatch.setattr(scheduler, "_perform_pending_cleanup", perform_pending_cleanup)
await scheduler._run_startup_tasks()
refresh_user_agent.assert_not_awaited()
perform_pending_cleanup.assert_not_awaited()
@pytest.mark.asyncio
async def test_maintenance_scheduler_startup_tasks_run_python_pending_cleanup_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scheduler = MaintenanceScheduler()
refresh_user_agent = AsyncMock()
perform_pending_cleanup = AsyncMock()
monkeypatch.setattr(config, "pending_cleanup_python_enabled", True)
monkeypatch.setattr(config, "antigravity_ua_refresh_python_enabled", True)
monkeypatch.setattr(maintenance_scheduler_module.asyncio, "sleep", AsyncMock())
monkeypatch.setattr(
"src.services.provider.adapters.antigravity.client.refresh_user_agent",
refresh_user_agent,
)
monkeypatch.setattr(scheduler, "_perform_pending_cleanup", perform_pending_cleanup)
await scheduler._run_startup_tasks()
refresh_user_agent.assert_awaited_once()
perform_pending_cleanup.assert_awaited_once()
@pytest.mark.asyncio
async def test_start_background_services_skips_python_maintenance_scheduler_when_unused(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, _quota_scheduler, _task_poller, _model_fetch_scheduler = (
_patch_background_services_dependencies(monkeypatch)
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "quota_scheduler_python_enabled", False)
monkeypatch.setattr(config, "video_task_python_poller_enabled", False)
monkeypatch.setattr(config, "model_fetch_scheduler_python_enabled", False)
monkeypatch.setattr(config, "audit_cleanup_python_enabled", False)
monkeypatch.setattr(config, "antigravity_ua_refresh_python_enabled", False)
monkeypatch.setattr(config, "db_maintenance_python_enabled", False)
monkeypatch.setattr(config, "gemini_file_mapping_cleanup_python_enabled", False)
monkeypatch.setattr(config, "http_client_idle_cleanup_python_enabled", False)
monkeypatch.setattr(config, "pending_cleanup_python_enabled", False)
monkeypatch.setattr(config, "pool_monitor_python_enabled", False)
monkeypatch.setattr(config, "provider_checkin_python_enabled", False)
monkeypatch.setattr(config, "request_candidate_cleanup_python_enabled", False)
monkeypatch.setattr(config, "stats_aggregation_python_enabled", False)
monkeypatch.setattr(config, "stats_hourly_aggregation_python_enabled", False)
monkeypatch.setattr(config, "usage_cleanup_python_enabled", False)
monkeypatch.setattr(config, "wallet_daily_usage_aggregation_python_enabled", False)
await main_module._start_background_services(state)
assert "maintenance_scheduler" not in coordinator.acquire_calls
assert state.maintenance_scheduler is None
@pytest.mark.asyncio
async def test_start_background_services_starts_python_maintenance_scheduler_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, _quota_scheduler, _task_poller, _model_fetch_scheduler = (
_patch_background_services_dependencies(
monkeypatch,
acquire_results={"maintenance_scheduler": True},
)
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "quota_scheduler_python_enabled", False)
monkeypatch.setattr(config, "video_task_python_poller_enabled", False)
monkeypatch.setattr(config, "model_fetch_scheduler_python_enabled", False)
monkeypatch.setattr(config, "http_client_idle_cleanup_python_enabled", True)
await main_module._start_background_services(state)
assert "maintenance_scheduler" in coordinator.acquire_calls
assert "maintenance_scheduler" in coordinator.registered_callbacks
assert state.maintenance_scheduler is not None
def _patch_core_infrastructure_dependencies(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(config, "log_startup_warnings", lambda: None)
monkeypatch.setattr(main_module, "init_db", lambda: None)
monkeypatch.setattr(main_module, "initialize_providers", AsyncMock())
monkeypatch.setattr(
"src.clients.redis_client.get_redis_client",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.rate_limit.concurrency_manager.get_concurrency_manager",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.services.rate_limit.user_rpm_limiter.get_user_rpm_limiter",
AsyncMock(return_value=None),
)
monkeypatch.setattr("src.core.batch_committer.init_batch_committer", AsyncMock())
monkeypatch.setattr(
"src.services.provider_keys.codex_quota_sync_dispatcher.init_codex_quota_sync_dispatcher",
AsyncMock(),
)
def _patch_python_host_shutdown_dependencies(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
"src.services.provider_keys.codex_quota_sync_dispatcher.shutdown_codex_quota_sync_dispatcher",
AsyncMock(),
)
monkeypatch.setattr("src.core.batch_committer.shutdown_batch_committer", AsyncMock())
monkeypatch.setattr("src.clients.redis_client.close_redis_client", AsyncMock())
monkeypatch.setattr(main_module, "close_http_clients", AsyncMock())
class _FakeStartupTaskCoordinator:
def __init__(self, acquire_results: dict[str, bool] | None = None) -> None:
self.acquire_results = acquire_results or {}
self.acquire_calls: list[str] = []
self.registered_callbacks: list[str] = []
self.released: list[str] = []
async def acquire(self, name: str) -> bool:
self.acquire_calls.append(name)
return self.acquire_results.get(name, False)
async def release(self, name: str) -> None:
self.released.append(name)
def register_lock_lost_callback(self, name: str, _callback: Any) -> None:
self.registered_callbacks.append(name)
def _patch_background_services_dependencies(
monkeypatch: pytest.MonkeyPatch,
*,
acquire_results: dict[str, bool] | None = None,
) -> tuple[_FakeStartupTaskCoordinator, Any, Any, Any]:
coordinator = _FakeStartupTaskCoordinator(acquire_results)
quota_scheduler = SimpleNamespace(start=AsyncMock(), stop=AsyncMock())
maintenance_scheduler = SimpleNamespace(start=AsyncMock(), stop=AsyncMock())
model_fetch_scheduler = SimpleNamespace(start=AsyncMock(), stop=AsyncMock())
pool_quota_probe_scheduler = SimpleNamespace(start=AsyncMock(), stop=AsyncMock())
task_poller = SimpleNamespace(start=AsyncMock(), stop=AsyncMock())
task_scheduler = SimpleNamespace(start=MagicMock())
monkeypatch.setattr(
"src.utils.task_coordinator.StartupTaskCoordinator",
lambda _redis: coordinator,
)
monkeypatch.setattr(
"src.services.usage.quota_scheduler.get_quota_scheduler",
lambda: quota_scheduler,
)
monkeypatch.setattr(
"src.services.system.maintenance_scheduler.get_maintenance_scheduler",
lambda: maintenance_scheduler,
)
monkeypatch.setattr(
"src.services.model.fetch_scheduler.get_model_fetch_scheduler",
lambda: model_fetch_scheduler,
)
monkeypatch.setattr(
"src.services.provider_keys.pool_quota_probe_scheduler.get_pool_quota_probe_scheduler",
lambda: pool_quota_probe_scheduler,
)
monkeypatch.setattr(
"src.services.task.polling.task_poller.get_task_poller",
lambda: task_poller,
)
monkeypatch.setattr(
"src.services.system.scheduler.get_scheduler",
lambda: task_scheduler,
)
return coordinator, quota_scheduler, task_poller, model_fetch_scheduler
@pytest.mark.asyncio
async def test_initialize_core_infrastructure_skips_python_usage_consumer_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_core_infrastructure_dependencies(monkeypatch)
started = AsyncMock()
monkeypatch.setattr(config, "require_redis", False)
monkeypatch.setattr(config, "usage_queue_enabled", True)
monkeypatch.setattr(config, "usage_queue_python_consumer_enabled", False)
monkeypatch.setattr("src.services.usage.consumer_streams.start_usage_queue_consumer", started)
await main_module._initialize_core_infrastructure(main_module.LifecycleState())
started.assert_not_awaited()
@pytest.mark.asyncio
async def test_initialize_core_infrastructure_starts_python_usage_consumer_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_core_infrastructure_dependencies(monkeypatch)
started = AsyncMock()
monkeypatch.setattr(config, "require_redis", False)
monkeypatch.setattr(config, "usage_queue_enabled", True)
monkeypatch.setattr(config, "usage_queue_python_consumer_enabled", True)
monkeypatch.setattr("src.services.usage.consumer_streams.start_usage_queue_consumer", started)
await main_module._initialize_core_infrastructure(main_module.LifecycleState())
started.assert_awaited_once()
@pytest.mark.asyncio
async def test_run_python_host_shutdown_skips_python_usage_consumer_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_python_host_shutdown_dependencies(monkeypatch)
stopped = AsyncMock()
monkeypatch.setattr(config, "usage_queue_enabled", True)
monkeypatch.setattr(config, "usage_queue_python_consumer_enabled", False)
monkeypatch.setattr("src.services.usage.consumer_streams.stop_usage_queue_consumer", stopped)
await main_module._run_python_host_shutdown(main_module.LifecycleState())
stopped.assert_not_awaited()
@pytest.mark.asyncio
async def test_run_python_host_shutdown_stops_python_usage_consumer_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_patch_python_host_shutdown_dependencies(monkeypatch)
stopped = AsyncMock()
monkeypatch.setattr(config, "usage_queue_enabled", True)
monkeypatch.setattr(config, "usage_queue_python_consumer_enabled", True)
monkeypatch.setattr("src.services.usage.consumer_streams.stop_usage_queue_consumer", stopped)
await main_module._run_python_host_shutdown(main_module.LifecycleState())
stopped.assert_awaited_once()
@pytest.mark.asyncio
async def test_start_background_services_skips_python_video_poller_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, _quota_scheduler, task_poller, _model_fetch_scheduler = _patch_background_services_dependencies(
monkeypatch
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "video_task_python_poller_enabled", False)
await main_module._start_background_services(state)
assert "task_poller:video" not in coordinator.acquire_calls
task_poller.start.assert_not_awaited()
assert state.task_poller is None
@pytest.mark.asyncio
async def test_start_background_services_starts_python_video_poller_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, _quota_scheduler, task_poller, _model_fetch_scheduler = _patch_background_services_dependencies(
monkeypatch,
acquire_results={"task_poller:video": True},
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "video_task_python_poller_enabled", True)
await main_module._start_background_services(state)
assert "task_poller:video" in coordinator.acquire_calls
assert "task_poller:video" in coordinator.registered_callbacks
task_poller.start.assert_awaited_once()
assert state.task_poller is task_poller
@pytest.mark.asyncio
async def test_start_background_services_skips_python_quota_scheduler_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, quota_scheduler, _task_poller, _model_fetch_scheduler = _patch_background_services_dependencies(
monkeypatch
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "quota_scheduler_python_enabled", False)
monkeypatch.setattr(config, "video_task_python_poller_enabled", False)
await main_module._start_background_services(state)
assert "quota_scheduler" not in coordinator.acquire_calls
quota_scheduler.start.assert_not_awaited()
assert state.quota_scheduler is None
@pytest.mark.asyncio
async def test_start_background_services_starts_python_quota_scheduler_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, quota_scheduler, _task_poller, _model_fetch_scheduler = _patch_background_services_dependencies(
monkeypatch,
acquire_results={"quota_scheduler": True},
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "quota_scheduler_python_enabled", True)
monkeypatch.setattr(config, "video_task_python_poller_enabled", False)
await main_module._start_background_services(state)
assert "quota_scheduler" in coordinator.acquire_calls
assert "quota_scheduler" in coordinator.registered_callbacks
quota_scheduler.start.assert_awaited_once()
assert state.quota_scheduler is quota_scheduler
@pytest.mark.asyncio
async def test_start_background_services_skips_python_model_fetch_scheduler_when_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, _quota_scheduler, _task_poller, model_fetch_scheduler = (
_patch_background_services_dependencies(monkeypatch)
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "quota_scheduler_python_enabled", False)
monkeypatch.setattr(config, "model_fetch_scheduler_python_enabled", False)
monkeypatch.setattr(config, "video_task_python_poller_enabled", False)
await main_module._start_background_services(state)
assert "model_fetch_scheduler" not in coordinator.acquire_calls
model_fetch_scheduler.start.assert_not_awaited()
assert state.model_fetch_scheduler is None
@pytest.mark.asyncio
async def test_start_background_services_starts_python_model_fetch_scheduler_when_enabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
coordinator, _quota_scheduler, _task_poller, model_fetch_scheduler = (
_patch_background_services_dependencies(
monkeypatch,
acquire_results={"model_fetch_scheduler": True},
)
)
state = main_module.LifecycleState()
monkeypatch.setattr(config, "quota_scheduler_python_enabled", False)
monkeypatch.setattr(config, "model_fetch_scheduler_python_enabled", True)
monkeypatch.setattr(config, "video_task_python_poller_enabled", False)
await main_module._start_background_services(state)
assert "model_fetch_scheduler" in coordinator.acquire_calls
assert "model_fetch_scheduler" in coordinator.registered_callbacks
model_fetch_scheduler.start.assert_awaited_once()
assert state.model_fetch_scheduler is model_fetch_scheduler
@pytest.mark.asyncio
async def test_stop_service_on_lock_lost_keeps_state_when_stop_fails() -> None:
state = main_module.LifecycleState()

View File

@@ -191,7 +191,7 @@ async def test_fetch_models_from_endpoints_uses_rust_for_gemini(
@pytest.mark.asyncio
async def test_fetch_models_from_endpoints_falls_back_to_python_when_rust_unavailable(
async def test_fetch_models_from_endpoints_returns_rust_only_error_when_rust_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(fetcher_mod.config, "executor_backend", "rust")
@@ -224,7 +224,7 @@ async def test_fetch_models_from_endpoints_falls_back_to_python_when_rust_unavai
timeout=1.0,
)
assert ok is True
assert errors == []
assert models == [{"id": "fallback-model", "api_format": "openai:chat"}]
python_fetch.assert_awaited_once()
assert ok is False
assert models == []
assert errors == ["openai:chat: rust executor unavailable"]
python_fetch.assert_not_awaited()

View File

@@ -23,7 +23,7 @@ class _TimeoutAsyncClient:
@pytest.mark.asyncio
async def test_vertex_auth_timeout_error_includes_readable_message(
async def test_vertex_auth_is_rust_only(
monkeypatch: pytest.MonkeyPatch,
) -> None:
service = VertexAuthService(
@@ -39,5 +39,5 @@ async def test_vertex_auth_timeout_error_includes_readable_message(
monkeypatch.setattr(service, "_create_jwt", lambda: "signed-jwt")
monkeypatch.setattr("src.core.vertex_auth.httpx.AsyncClient", _TimeoutAsyncClient)
with pytest.raises(VertexAuthError, match=r"request timed out after 30s"):
with pytest.raises(VertexAuthError, match=r"仅支持 Rust executor"):
await service.get_access_token(httpx_client_kwargs={"timeout": 30})

View File

@@ -3,6 +3,7 @@ from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import httpx
import pytest
import src.services.task.video.cancel as cancel_mod
@@ -171,3 +172,122 @@ async def test_video_cancel_service_uses_rust_for_gemini_cancel(
plan = execute_sync.await_args.args[0]
assert plan.method == "POST"
assert plan.body.json_body == {}
@pytest.mark.asyncio
async def test_video_cancel_service_returns_503_when_rust_backend_disabled(
monkeypatch: pytest.MonkeyPatch,
) -> None:
endpoint = SimpleNamespace(
id="ep-1",
provider_id="prov-1",
api_family="openai",
endpoint_kind="video",
base_url="https://api.openai.com",
)
key = SimpleNamespace(id="key-1", api_key="encrypted")
db = _FakeDB(endpoint, key)
service = VideoTaskCancelService(db)
task = SimpleNamespace(
id="task-1",
status=VideoStatus.PROCESSING.value,
external_task_id="ext-1",
endpoint_id="ep-1",
key_id="key-1",
request_id="req-1",
model="sora-2",
completed_at=None,
updated_at=None,
)
monkeypatch.setattr(cancel_mod.config, "executor_backend", "python")
monkeypatch.setattr(
"src.core.crypto.crypto_service.decrypt",
lambda value: "upstream-key",
)
monkeypatch.setattr(
"src.core.api_format.build_upstream_headers_for_endpoint",
lambda *args, **kwargs: {"authorization": "Bearer upstream-key"},
)
monkeypatch.setattr(
"src.services.provider.transport.build_provider_url",
lambda endpoint, is_stream=False, key=None: "https://api.openai.com/v1/videos",
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_default_client_async",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
response = await service.cancel_task(
task=task,
task_id="task-1",
original_headers={"x-test": "1"},
)
assert isinstance(response, httpx.Response)
assert response.status_code == 503
assert response.json() == {"error": {"message": "Video 取消仅支持 Rust executor"}}
assert db.committed is False
assert task.status == VideoStatus.PROCESSING.value
@pytest.mark.asyncio
async def test_video_cancel_service_returns_503_when_rust_executor_unavailable(
monkeypatch: pytest.MonkeyPatch,
) -> None:
endpoint = SimpleNamespace(
id="ep-1",
provider_id="prov-1",
api_family="gemini",
endpoint_kind="video",
base_url="https://generativelanguage.googleapis.com",
)
key = SimpleNamespace(id="key-1", api_key="encrypted")
db = _FakeDB(endpoint, key)
service = VideoTaskCancelService(db)
task = SimpleNamespace(
id="task-1",
status=VideoStatus.PROCESSING.value,
external_task_id="operations/ext-1",
endpoint_id="ep-1",
key_id="key-1",
request_id="req-1",
model="veo-3",
completed_at=None,
updated_at=None,
)
monkeypatch.setattr(cancel_mod.config, "executor_backend", "rust")
monkeypatch.setattr(
rust_client_mod.RustExecutorClient,
"execute_sync_json",
AsyncMock(side_effect=rust_client_mod.RustExecutorClientError("executor down")),
)
monkeypatch.setattr(
"src.core.crypto.crypto_service.decrypt",
lambda value: "upstream-key",
)
monkeypatch.setattr(
"src.core.api_format.build_upstream_headers_for_endpoint",
lambda *args, **kwargs: {"x-goog-api-key": "upstream-key"},
)
monkeypatch.setattr(
"src.services.provider.auth.get_provider_auth",
AsyncMock(return_value=None),
)
monkeypatch.setattr(
"src.clients.http_client.HTTPClientPool.get_default_client_async",
AsyncMock(side_effect=AssertionError("python fallback should not run")),
)
response = await service.cancel_task(
task=task,
task_id="task-1",
original_headers={"x-test": "1"},
)
assert isinstance(response, httpx.Response)
assert response.status_code == 503
assert response.json() == {"error": {"message": "执行器暂时不可用,请稍后重试"}}
assert db.committed is False
assert task.status == VideoStatus.PROCESSING.value

View File

@@ -25,30 +25,26 @@ async def test_poll_task_status_routes_gemini_video_to_gemini(
external_task_id="operations/123",
),
)
endpoint = SimpleNamespace(id="e1", base_url="https://example.com", api_format="gemini:video")
key = SimpleNamespace(id="k1", api_key="enc")
monkeypatch.setattr(adapter, "_get_endpoint", lambda _db, _id: endpoint)
monkeypatch.setattr(adapter, "_get_key", lambda _db, _id: key)
monkeypatch.setattr(
"src.services.task.video.poller_adapter.crypto_service.decrypt", lambda _v: "decrypted"
prepared_ctx = VideoPollContext(
task_id="task-1",
external_task_id="operations/123",
provider_api_format="gemini:video",
base_url="https://example.com",
upstream_key="decrypted",
headers={"authorization": "Bearer x"},
poll_count=0,
retry_count=0,
poll_interval_seconds=15,
max_poll_count=10,
current_status=VideoStatus.PROCESSING.value,
)
auth_info = SimpleNamespace(auth_header="authorization", auth_value="Bearer x")
monkeypatch.setattr(
"src.services.task.video.poller_adapter.get_provider_auth",
AsyncMock(return_value=auth_info),
)
poll_gemini = AsyncMock(return_value=InternalVideoPollResult(status=VideoStatus.PROCESSING))
poll_openai = AsyncMock(return_value=InternalVideoPollResult(status=VideoStatus.PROCESSING))
monkeypatch.setattr(adapter, "_poll_gemini", poll_gemini)
monkeypatch.setattr(adapter, "_poll_openai", poll_openai)
poll_http = AsyncMock(return_value=InternalVideoPollResult(status=VideoStatus.PROCESSING))
monkeypatch.setattr(adapter, "prepare_poll_context", AsyncMock(return_value=prepared_ctx))
monkeypatch.setattr(adapter, "poll_task_http", poll_http)
result = await adapter._poll_task_status(MagicMock(), task)
assert result.status == VideoStatus.PROCESSING
assert poll_gemini.await_count == 1
assert poll_openai.await_count == 0
poll_http.assert_awaited_once_with(prepared_ctx)
@pytest.mark.asyncio
@@ -129,8 +125,6 @@ async def test_video_poller_try_rust_payload_passes_proxy_snapshot(
async def test_video_poller_openai_poll_prefers_rust_payload(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.services.task.video import poller_adapter as mod
adapter = VideoTaskPollerAdapter()
rust_poll = AsyncMock(return_value={"id": "vid_1", "status": "processing"})
@@ -140,9 +134,6 @@ async def test_video_poller_openai_poll_prefers_rust_payload(
normalizer = MagicMock(return_value=normalized)
monkeypatch.setattr(adapter._openai_normalizer, "video_poll_to_internal", normalizer)
get_upstream_client = AsyncMock(side_effect=AssertionError("python upstream client should not be used"))
monkeypatch.setattr(mod.HTTPClientPool, "get_upstream_client", get_upstream_client)
ctx = VideoPollContext(
task_id="task-1",
external_task_id="vid_1",
@@ -161,34 +152,17 @@ async def test_video_poller_openai_poll_prefers_rust_payload(
assert result is normalized
normalizer.assert_called_once_with({"id": "vid_1", "status": "processing"})
get_upstream_client.assert_not_awaited()
rust_poll.assert_awaited_once()
@pytest.mark.asyncio
async def test_video_poller_openai_poll_fallback_uses_transport_aware_client(
async def test_video_poller_openai_poll_requires_rust_executor_when_payload_missing(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.services.task.video import poller_adapter as mod
adapter = VideoTaskPollerAdapter()
monkeypatch.setattr(adapter, "_try_rust_poll_payload", AsyncMock(return_value=None))
response = MagicMock()
response.status_code = 200
response.json.return_value = {"id": "vid_2", "status": "processing"}
client = MagicMock()
client.get = AsyncMock(return_value=response)
get_upstream_client = AsyncMock(return_value=client)
monkeypatch.setattr(mod.HTTPClientPool, "get_upstream_client", get_upstream_client)
normalized = InternalVideoPollResult(status=VideoStatus.PROCESSING, progress_percent=7)
normalizer = MagicMock(return_value=normalized)
monkeypatch.setattr(adapter._openai_normalizer, "video_poll_to_internal", normalizer)
ctx = VideoPollContext(
task_id="task-2",
external_task_id="vid_2",
@@ -205,14 +179,7 @@ async def test_video_poller_openai_poll_fallback_uses_transport_aware_client(
delegate_config={"node_id": "node-1", "tunnel": True},
)
result = await adapter._poll_openai_with_context(ctx)
with pytest.raises(Exception) as exc_info:
await adapter._poll_openai_with_context(ctx)
assert result is normalized
get_upstream_client.assert_awaited_once_with(
{"node_id": "node-1", "tunnel": True},
proxy_config={"enabled": True, "url": "http://proxy.test:8080"},
)
client.get.assert_awaited_once_with(
"https://api.openai.com/v1/videos/vid_2",
headers={"authorization": "Bearer test"},
)
assert "Rust executor" in str(exc_info.value)

View File

@@ -79,3 +79,14 @@ def test_import_new_standalone_key_null_rate_limit_keeps_inherit_semantics() ->
)
is None
)
def test_import_access_list_accepts_stringified_json_array() -> None:
assert AdminImportUsersAdapter._normalize_imported_access_list('["openai", " gemini "]') == [
"openai",
"gemini",
]
def test_import_access_list_wraps_single_string() -> None:
assert AdminImportUsersAdapter._normalize_imported_access_list("openai") == ["openai"]

View File

@@ -58,6 +58,8 @@ def _build_context(raw_body: bytes, headers: dict[str, str] | None = None) -> Ap
api_key=None,
request_id="req_test",
start_time=0.0,
request_method="POST",
request_path="/v1/messages",
client_ip="127.0.0.1",
user_agent="pytest",
original_headers=headers or {},
@@ -67,6 +69,74 @@ def _build_context(raw_body: bytes, headers: dict[str, str] | None = None) -> Ap
class TestApiRequestContextEnsureJsonBody:
def test_build_prefers_request_state_request_id_over_trace_header(self) -> None:
request = _build_request(headers={"x-trace-id": "trace-frontdoor-123"})
request.state.request_id = "state-rid-001"
context = ApiRequestContext.build(
request=request,
db=None, # type: ignore[arg-type]
user=None,
api_key=None,
raw_body=b"{}",
)
assert context.request_id == "state-rid-001"
assert request.state.request_id == "state-rid-001"
def test_build_prefers_trace_header_for_request_id(self) -> None:
request = _build_request(headers={"x-trace-id": "trace-frontdoor-123"})
context = ApiRequestContext.build(
request=request,
db=None, # type: ignore[arg-type]
user=None,
api_key=None,
raw_body=b"{}",
)
assert context.request_id == "trace-frontdoor-123"
assert request.state.request_id == "trace-frontdoor-123"
def test_build_snapshots_request_method_path_and_path_params(self) -> None:
request = _build_request(headers={"x-trace-id": "trace-frontdoor-123"})
request.scope["method"] = "GET"
request.scope["path"] = "/v1beta/models/gemini-2.5-pro:generateContent"
request.scope["raw_path"] = b"/v1beta/models/gemini-2.5-pro:generateContent"
request.scope["path_params"] = {"model": "gemini-2.5-pro"}
context = ApiRequestContext.build(
request=request,
db=None, # type: ignore[arg-type]
user=None,
api_key=None,
raw_body=b"{}",
)
assert context.request_method == "GET"
assert context.request_path == "/v1beta/models/gemini-2.5-pro:generateContent"
assert context.path_params == {"model": "gemini-2.5-pro"}
def test_build_snapshots_request_runtime_state(self) -> None:
request = _build_request(headers={"x-trace-id": "trace-frontdoor-123"})
request.state.prefetched_balance_remaining = "12.5"
request.state.gateway_execution_path = "public_proxy_after_executor_miss"
request.state.rate_limit_scope = "user"
request.state.tx_committed_by_route = True
context = ApiRequestContext.build(
request=request,
db=None, # type: ignore[arg-type]
user=None,
api_key=None,
raw_body=b"{}",
)
assert context.prefetched_balance_remaining == 12.5
assert context.gateway_execution_path == "public_proxy_after_executor_miss"
assert context.rate_limit_scope == "user"
assert context.tx_committed_by_route is True
def test_decompresses_gzip_body(self) -> None:
payload = {"message": "hello", "count": 2}
raw_body = gzip.compress(json.dumps(payload).encode("utf-8"))
@@ -88,6 +158,7 @@ class TestApiRequestContextEnsureJsonBody:
def test_build_records_client_encoding_preferences(self) -> None:
request = _build_request(
headers={
"content-type": "application/json",
"content-encoding": "gzip",
"accept-encoding": "gzip, deflate",
}
@@ -102,6 +173,31 @@ class TestApiRequestContextEnsureJsonBody:
assert context.client_content_encoding == "gzip"
assert context.client_accept_encoding == "gzip, deflate"
assert context.request_content_type == "application/json"
def test_build_records_perf_only_when_payload_not_empty(self) -> None:
request = _build_request(headers={"x-trace-id": "trace-frontdoor-123"})
request.state.perf_metrics = {}
context = ApiRequestContext.build(
request=request,
db=None, # type: ignore[arg-type]
user=None,
api_key=None,
raw_body=b"{}",
)
assert "perf" not in context.extra
request_with_perf = _build_request(headers={"x-trace-id": "trace-frontdoor-456"})
request_with_perf.state.perf_metrics = {"pipeline": {"auth_ms": 3}}
context_with_perf = ApiRequestContext.build(
request=request_with_perf,
db=None, # type: ignore[arg-type]
user=None,
api_key=None,
raw_body=b"{}",
)
assert context_with_perf.extra["perf"] == {"pipeline": {"auth_ms": 3}}
assert context_with_perf.perf_metrics == {"pipeline": {"auth_ms": 3}}
@pytest.mark.asyncio
async def test_ensure_json_body_async_loads_body_lazily(self) -> None:

View File

@@ -9,6 +9,7 @@ from src.services.request.executor_plan import (
ExecutionPlanTimeouts,
ExecutionProxySnapshot,
build_execution_plan_body,
should_bypass_remote_executor_url,
)
@@ -342,3 +343,43 @@ def test_prepared_execution_plan_remote_eligible_allows_empty_get_body() -> None
)
assert prepared.remote_eligible is True
def test_prepared_execution_plan_remote_eligible_rejects_codex_cli_transport() -> None:
prepared = PreparedExecutionPlan(
contract=ExecutionPlan(
request_id="req-1",
candidate_id=None,
provider_name="codex",
provider_id="prov-1",
endpoint_id="ep-1",
key_id="key-1",
method="POST",
url="https://chatgpt.com/backend-api/codex/responses",
headers={"content-type": "application/json"},
body=ExecutionPlanBody(json_body={"model": "gpt-5.4", "input": []}),
stream=True,
provider_api_format="openai:cli",
client_api_format="openai:cli",
model_name="gpt-5.4",
),
payload={"model": "gpt-5.4", "input": []},
headers={"content-type": "application/json"},
upstream_is_stream=True,
needs_conversion=False,
provider_type="codex",
request_timeout=30.0,
)
assert prepared.remote_eligible is False
def test_should_bypass_remote_executor_url_accepts_backendapi_codex_variant() -> None:
assert (
should_bypass_remote_executor_url(
"https://chatgpt.com/backendapi/codex/responses",
provider_api_format="openai:cli",
client_api_format="openai:cli",
)
is True
)

View File

@@ -1,10 +1,43 @@
from __future__ import annotations
import httpx
import pytest
from types import SimpleNamespace
from src.api.admin import provider_oauth as module
class _SingleKeyQuery:
def __init__(self, key: object) -> None:
self._key = key
def filter(self, *_args: object, **_kwargs: object) -> "_SingleKeyQuery":
return self
def first(self) -> object:
return self._key
class _SingleKeyDB:
def __init__(self, key: object) -> None:
self._key = key
def query(self, _model: object) -> _SingleKeyQuery:
return _SingleKeyQuery(self._key)
class _FakeDBContext:
def __init__(self, db: _SingleKeyDB) -> None:
self._db = db
def __enter__(self) -> _SingleKeyDB:
return self._db
def __exit__(self, exc_type: object, exc: object, tb: object) -> bool:
_ = exc_type, exc, tb
return False
def test_extract_oauth_refresh_error_reason_for_reused_refresh_token() -> None:
response = httpx.Response(
400,
@@ -54,3 +87,50 @@ def test_merge_refresh_failure_reason_keeps_account_block_and_appends_refresh_fa
"[ACCOUNT_BLOCK] 工作区已停用 (deactivated_workspace)\n"
"[REFRESH_FAILED] Token 续期失败 (400): refresh_token_reused"
)
def test_merge_refresh_failure_reason_keeps_oauth_expired_sticky() -> None:
current_reason = "[OAUTH_EXPIRED] Token 已过期且续期失败"
refresh_reason = "[REFRESH_FAILED] Token 续期失败 (400): refresh_token_reused"
assert module._merge_refresh_failure_reason(current_reason, refresh_reason) is None
@pytest.mark.parametrize(
("initial_reason", "should_clear"),
[
("[REFRESH_FAILED] Token 续期失败 (401): refresh_token_reused", True),
("[ACCOUNT_BLOCK] Google requires verification", False),
],
)
def test_store_refreshed_oauth_sync_only_clears_recoverable_invalid_markers(
monkeypatch: pytest.MonkeyPatch,
initial_reason: str,
should_clear: bool,
) -> None:
key = SimpleNamespace(
id="key-1",
api_key="old-api",
auth_config="old-config",
oauth_invalid_at="old-invalid-at",
oauth_invalid_reason=initial_reason,
)
db = _SingleKeyDB(key)
monkeypatch.setattr(module, "get_db_context", lambda: _FakeDBContext(db))
monkeypatch.setattr(module.crypto_service, "encrypt", lambda value: f"enc:{value}")
module._store_refreshed_oauth_sync(
"key-1",
"new-token",
{"refresh_token": "rt-2"},
)
assert key.api_key == "enc:new-token"
assert key.auth_config == 'enc:{"refresh_token": "rt-2"}'
if should_clear:
assert key.oauth_invalid_at is None
assert key.oauth_invalid_reason is None
else:
assert key.oauth_invalid_at == "old-invalid-at"
assert key.oauth_invalid_reason == initial_reason

View File

@@ -0,0 +1,110 @@
from __future__ import annotations
from starlette.requests import Request
from src.utils.request_utils import (
get_request_id,
get_request_identity_metadata,
get_request_metadata,
update_request_state,
)
def _build_request(headers: dict[str, str] | None = None) -> Request:
header_items = [
(str(key).encode("latin-1"), str(value).encode("latin-1"))
for key, value in (headers or {}).items()
]
scope = {
"type": "http",
"http_version": "1.1",
"method": "GET",
"scheme": "http",
"path": "/health",
"raw_path": b"/health",
"query_string": b"",
"headers": header_items,
"client": ("127.0.0.1", 12345),
"server": ("testserver", 80),
}
async def receive() -> dict[str, object]:
return {"type": "http.request", "body": b"", "more_body": False}
return Request(scope, receive)
def test_get_request_id_prefers_request_state() -> None:
request = _build_request(headers={"x-trace-id": "trace-header-123"})
request.state.request_id = "req-state-123"
assert get_request_id(request) == "req-state-123"
def test_get_request_id_falls_back_to_trace_header() -> None:
request = _build_request(headers={"x-trace-id": "trace-header-123"})
assert get_request_id(request) == "trace-header-123"
def test_get_request_id_returns_none_without_state_or_trace_header() -> None:
request = _build_request()
assert get_request_id(request) is None
def test_update_request_state_sets_selected_fields() -> None:
request = _build_request()
update_request_state(
request,
request_id="req-123",
user_id="user-123",
api_key_id="key-123",
gateway_execution_path="executor_sync",
rate_limit_scope="user",
)
assert request.state.request_id == "req-123"
assert request.state.user_id == "user-123"
assert request.state.api_key_id == "key-123"
assert request.state.gateway_execution_path == "executor_sync"
assert request.state.rate_limit_scope == "user"
def test_get_request_identity_metadata_reads_request_id_client_ip_and_user_agent() -> None:
request = _build_request(
headers={
"x-trace-id": "trace-header-abc",
"x-real-ip": "203.0.113.7",
"user-agent": "pytest-agent",
}
)
meta = get_request_identity_metadata(request)
assert meta.request_id == "trace-header-abc"
assert meta.client_ip == "203.0.113.7"
assert meta.user_agent == "pytest-agent"
def test_get_request_metadata_reuses_identity_fields() -> None:
request = _build_request(
headers={
"x-trace-id": "trace-xyz",
"x-real-ip": "198.51.100.23",
"user-agent": "pytest-meta-agent",
"content-type": "application/json",
"content-length": "42",
}
)
metadata = get_request_metadata(request)
assert metadata["request_id"] == "trace-xyz"
assert metadata["client_ip"] == "198.51.100.23"
assert metadata["user_agent"] == "pytest-meta-agent"
assert metadata["method"] == "GET"
assert metadata["path"] == "/health"
assert metadata["content_type"] == "application/json"
assert metadata["content_length"] == "42"