feat(ai): gRPC clients 扩展 + server interceptors + proto_gen + 测试 + nextstep 文档

This commit is contained in:
SpecialX
2026-07-14 15:59:41 +08:00
parent fb23c5234e
commit 7b790f1276
21 changed files with 3540 additions and 345 deletions

View File

@@ -37,17 +37,24 @@ from src.ai.middleware.error_handler import (
@pytest.fixture
async def client() -> AsyncGenerator[httpx.AsyncClient, None]:
"""HTTP client wired to the FastAPI app (dev_mode=True, permissions skipped)."""
from src.ai.main import _permission_guard, app
"""HTTP client wired to the FastAPI app (dev_mode=True, permissions skipped).
注入 ContentClientMock 到 workflow_service因为测试环境无真实 content gRPC server。
"""
from src.ai.clients import ContentClientMock
from src.ai.main import _permission_guard, _workflow_service, app
original = _permission_guard._dev_mode
original_content = _workflow_service._content_client # noqa: SLF001
_permission_guard._dev_mode = True
_workflow_service._content_client = ContentClientMock() # noqa: SLF001
try:
transport = ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
yield c
finally:
_permission_guard._dev_mode = original
_workflow_service._content_client = original_content # noqa: SLF001
@pytest.fixture
@@ -136,14 +143,14 @@ async def test_handle_unknown_error() -> None:
async def test_grpc_error_mapper_ai_error() -> None:
"""grpc_error_mapper maps AIError to correct gRPC status code."""
cases = [
(ErrorCode.AI_UNAUTHORIZED, 8), # UNAUTHENTICATED
(ErrorCode.AI_FORBIDDEN, 7), # PERMISSION_DENIED
(ErrorCode.AI_RATE_LIMITED, 9), # RESOURCE_EXHAUSTED
(ErrorCode.AI_QUOTA_EXCEEDED, 9), # RESOURCE_EXHAUSTED
(ErrorCode.AI_INVALID_MODEL, 3), # INVALID_ARGUMENT
(ErrorCode.AI_UNAUTHORIZED, 8), # UNAUTHENTICATED
(ErrorCode.AI_FORBIDDEN, 7), # PERMISSION_DENIED
(ErrorCode.AI_RATE_LIMITED, 9), # RESOURCE_EXHAUSTED
(ErrorCode.AI_QUOTA_EXCEEDED, 9), # RESOURCE_EXHAUSTED
(ErrorCode.AI_INVALID_MODEL, 3), # INVALID_ARGUMENT
(ErrorCode.AI_WORKFLOW_NOT_FOUND, 5), # NOT_FOUND
(ErrorCode.AI_WORKFLOW_STATE_INVALID, 10), # FAILED_PRECONDITION
(ErrorCode.AI_INTERNAL_ERROR, 13), # INTERNAL
(ErrorCode.AI_INTERNAL_ERROR, 13), # INTERNAL
]
for code, expected_grpc_status in cases:
exc = AIError(code, f"test {code.value}")
@@ -390,9 +397,7 @@ async def test_confirm_lesson_plan_success(client: httpx.AsyncClient) -> None:
break
await asyncio.sleep(0.1)
assert status == "pending_review", (
f"Workflow did not reach pending_review, got: {status}"
)
assert status == "pending_review", f"Workflow did not reach pending_review, got: {status}"
resp = await client.post(f"/v1/ai/lesson-plan/confirm/{workflow_id}")
assert resp.status_code == 200