Files
geMoldInsight/tests/test_route_load_status.py
cjw 0e6b3b1811 后端设计治理:批次 0-4 全部完成(安全/部署/一致性/结构/架构)
按 ROADMAP §3.1 治理批次推进的后端设计审查整改:

- 批次 0(安全):/api/status/{task_id} 补 JWT 鉴权与任务归属校验;
  pythonocc_available 真实探测;bcrypt 超 72 字节显式拒绝;
  SECRET_KEY/RUSTFS_* 惰性校验,代码侧弱默认移除
- 批次 1(部署正确性):主处理链路改走 RustFS(分派入参 stp_file_id 化,
  worker 按 object_key 下载);AUTO_MIGRATE 开关 + 迁移目录 alembic/→migrations/
  修复包遮蔽(自动迁移此前从未真正生效);OCC 镜像改 conda 原生执行 +
  基础镜像 tag 锁定;compose 关键项改 ${VAR:?} 强制显式配置
- 批次 2(任务一致性):删除 Redis 进程内存回退,PG 为任务状态单一事实源;
  批量元数据入库(processing_tasks.batch_id,迁移 a3f8c2d91e47);
  型腔失败任务标 failed 不再静默 completed;事务边界收口
  (数据本体写 flush-only、失败先回滚再置 failed、进度更新保留即时 commit)
- 批次 3(API 与代码结构):592 行 advanced_router 拆为 design/cost/machining/
  export 四子路由,请求体全量 Pydantic 化;ROUTE_MODULES + route_registry
  (/api/health 呈现 degraded,DEBUG fail fast);纯计算端点统一 to_thread;
  StorageIntegrationService 按职责三拆;MAX_FILE_SIZE 接线生效、
  celery 复用 Settings.redis_url;管理员重置密码改 JSON body(端到端断裂修复);
  openapi.json 重导出(76 paths)+ 前端 gen:api
- 批次 4(架构演进):共享 ORM 按模块拆分(shared/models/base.py + identity.py、
  moldinsight/models/、inventory/models/,删除三条无使用方的跨模块
  relationship,跨模块桥接收敛为裸 FK 硬规则,无兼容 facade);
  OCC executor 重建补 cancel_futures=True(消除旧队列被慢恢复线程
  并行消化的数据竞争);OCC 吞吐方案设计先行
  (docs/topics/performance/OCC_THROUGHPUT.md);顺手清偿 D15
  (vite.config.ts 未用参数致 npm run build 失败)

测试基线:125 passed, 2 skipped(pytest + sqlite+aiosqlite;归属边界、
路由契约、配置治理、鉴权回归等随批新增)
文档同步:STATUS / TECH_DEBT / ROADMAP / ARCHITECTURE / API_CONTRACT /
OPERATIONS / AGENTS

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 16:15:49 +08:00

78 lines
3.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""批次 3 回归:路由装载失败显式化(_safe_include 不再静默跳过)。
- 非 DEBUG:失败记录进 route_registry,/api/health 呈现 degraded
- DEBUG:装载失败直接抛错(fail fast,开发期当场暴露)
- 注册表状态在测试间隔离(真实 app 在导入期已登记 loaded 列表)
"""
import pytest
from fastapi import FastAPI
from httpx import AsyncClient, ASGITransport
from shared.config.settings import settings
from moldinsight.api import _safe_include
from moldinsight.api.route_registry import route_load_status
from moldinsight.api.health_router import router as health_router
@pytest.fixture(autouse=True)
def clean_registry():
"""清空注册表并在测试后还原真实 app 导入期登记的状态。"""
snapshot = {k: list(v) for k, v in route_load_status.items()}
for items in route_load_status.values():
items.clear()
yield
for items in route_load_status.values():
items.clear()
for key, items in snapshot.items():
route_load_status[key].extend(items)
def test_failed_route_recorded_not_silent(monkeypatch):
"""非 DEBUG:装载失败必须留下显式记录(此前仅 WARNING 日志后静默跳过)。"""
monkeypatch.setattr(settings, "DEBUG", False)
_safe_include("不存在", "moldinsight.api.definitely_missing_module")
failed = route_load_status["failed"]
assert len(failed) == 1
assert failed[0]["label"] == "不存在"
assert failed[0]["module"] == "moldinsight.api.definitely_missing_module"
assert "error" in failed[0]
def test_failed_route_raises_in_debug(monkeypatch):
"""DEBUG:装载失败直接抛错,禁止带病启动。"""
monkeypatch.setattr(settings, "DEBUG", True)
with pytest.raises(ModuleNotFoundError):
_safe_include("不存在", "moldinsight.api.definitely_missing_module")
def test_debug_only_route_disabled_when_debug_off(monkeypatch):
monkeypatch.setattr(settings, "DEBUG", False)
_safe_include("调试", "moldinsight.api.debug_router", debug_only=True)
assert route_load_status["disabled"][0]["module"] == "moldinsight.api.debug_router"
assert route_load_status["failed"] == []
@pytest.mark.asyncio
async def test_health_reports_degraded_and_failed_routes(monkeypatch):
"""health 必须暴露失败路由清单并将 status 置为 degraded。"""
monkeypatch.setattr(settings, "DEBUG", False)
_safe_include("不存在", "moldinsight.api.definitely_missing_module")
test_app = FastAPI()
test_app.include_router(health_router, prefix="/api")
transport = ASGITransport(app=test_app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
resp = await ac.get("/api/health")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "degraded"
assert body["routes"]["failed"][0]["module"] == "moldinsight.api.definitely_missing_module"
# 真实探测而非硬编码(布尔类型即可,具体值取决于环境是否安装 OCC)
assert isinstance(body["pythonocc"], bool)