# ============================================================ # OpenAPI schema 生成门禁(round-52 回归守护) # # 根因:`server/gateway/app.py` 的 create_app() 函数体内定义的局部 # Pydantic 请求模型类,会让 FastAPI 生成 /openapi.json 时抛 # PydanticUserError(ForwardRef 无法按模块全局解析),线上表现为 # GET /openapi.json 500。 # # 本测试守护两件事: # 1) openapi() 必须能完整生成(所有路由请求模型可解析); # 2) 曾出问题的 12 个请求模型必须是模块级定义(可被 # `from server.gateway.app import ...` 导入,且 __qualname__ # 不含 create_app)。 # ============================================================ from __future__ import annotations import pytest from tests.auth_provider import install_test_auth @pytest.fixture() def gateway_app(tmp_path, monkeypatch): monkeypatch.setenv("APS_DB_PATH", str(tmp_path / "openapi.db")) monkeypatch.setenv("APS_WORLD_PATH", str(tmp_path / "world.json")) from server.db.database import reset_engine from server.state import store as world_store install_test_auth(monkeypatch, "tenant-a-000000000000000000000000001") world_store._stores.clear() reset_engine() from server.gateway.app import create_app app = create_app() yield app reset_engine() world_store._stores.clear() # round-52 曾定义在 create_app() 局部、导致 OpenAPI 生成崩溃的 12 个模型 MODULE_SCOPE_MODELS = ( "ImportCommitRequest", "TemplateApplyRequest", "RagQueryRequest", "RebuildStartRequest", "RebuildRollbackRequest", "KnowledgeImportRequest", "SkillRegisterRequest", "SkillEnableRequest", "McpPluginRegisterRequest", "McpPluginEnableRequest", "McpPermissionRequest", "JobSubmitRequest", ) # 上述模型服务的受影响路由 AFFECTED_PATHS = ( "/api/import/commit", "/api/templates/apply", "/api/rag/query", "/api/rebuild/{module_id}", "/api/rebuild/{module_id}/rollback", "/api/knowledge/import", "/api/skills/register", "/api/skills/{skill_id}/enable", "/api/mcp/plugins/register", "/api/mcp/plugins/{plugin_id}/enable", "/api/mcp/plugins/{plugin_id}/permissions", "/api/jobs", ) def test_openapi_schema_generates_without_forward_ref_error(gateway_app): """round-52 回归:openapi() 必须能完整生成,不再抛 PydanticUserError。""" spec = gateway_app.openapi() assert isinstance(spec, dict) paths = spec.get("paths") or {} for path in AFFECTED_PATHS: assert path in paths, f"openapi schema 缺少受影响路由 {path}" def test_affected_request_models_are_module_scope(gateway_app): """round-52 守护:请求模型必须是模块级定义,不能是 create_app 局部类。""" import server.gateway.app as gateway_module for name in MODULE_SCOPE_MODELS: cls = getattr(gateway_module, name) assert "create_app" not in cls.__qualname__, ( f"{name} 仍是函数局部定义: {cls.__qualname__}" ) assert cls.__module__ == "server.gateway.app", cls.__module__