Files
Profeto/tests/test_p1_d_concurrency.py
T
shangfangjian b997c06ede fix(P1-D): 全局 LLM 并发限制 + 删除未接线的 provider 字段
- 删除 PredictRequest.provider(未接线,路由从未传入,符合禁令 #7)
- 新增 _GLOBAL_LLM_SEMAPHORE(默认 4) + _predict_with_concurrency:
  公开 /predict 所有模式汇总受全局并发限制,与 orchestrator 内 match 级
  Semaphore(8) 并存。

测试 test_p1_d_concurrency(4/4);全量 313 通过。
2026-09-22 03:44:09 +08:00

72 lines
2.9 KiB
Python

"""P1-D 回归测试: 全局 LLM 并发限制 + provider 字段已删除。
运行: pytest tests/test_p1_d_concurrency.py -v
"""
import asyncio
from unittest.mock import patch
import pytest
from src.api.routes import predict as predict_mod
class TestGlobalLLMConcurrency:
"""P1-D: 全局 LLM 并发限制(默认 4)。"""
def test_semaphore_exists_with_limit(self):
"""路由模块必须存在 _GLOBAL_LLM_SEMAPHORE 且 value <= 4。"""
assert hasattr(predict_mod, "_GLOBAL_LLM_SEMAPHORE")
sem = predict_mod._GLOBAL_LLM_SEMAPHORE
assert isinstance(sem, asyncio.Semaphore)
assert sem._value == 4, f"期望并发限制 4,实际 {sem._value}"
def test_predict_with_concurrency_limits_parallel(self):
"""P1-D: 并发调用 _predict_with_concurrency 不得超过信号量限制。"""
max_concurrent = 0
current = 0
lock = asyncio.Lock()
async def fake_predict(match_id, **kwargs):
nonlocal current, max_concurrent
async with lock:
current += 1
max_concurrent = max(max_concurrent, current)
await asyncio.sleep(0.05) # 模拟 LLM 调用
async with lock:
current -= 1
return type("R", (), {"prediction_id": 1, "provider": "p", "model": "m",
"prompt_version": "v1", "pred_home_goals": 1.0,
"pred_away_goals": 0.0, "pred_1x2": "1",
"subjective_confidence": 0.5, "reasoning": "",
"status": "success", "context": "",
"latency_ms": 0, "raw": {}})()
req = type("Req", (), {"match_id": 1, "model": None, "prompt_version": None, "mode": "single"})()
async def run():
with patch.object(predict_mod, "predict_match", fake_predict):
# 启动 10 个并发请求
tasks = [predict_mod._predict_with_concurrency(req) for _ in range(10)]
await asyncio.gather(*tasks)
asyncio.run(run())
# 最大并发不得超过信号量限制(4)
assert max_concurrent <= 4, f"并发 {max_concurrent} 超过限制 4"
class TestProviderFieldRemoved:
"""P1-D: PredictRequest 的 provider 字段必须已删除(未接线)。"""
def test_predict_request_no_provider(self):
from src.api.schemas import PredictRequest
fields = set(PredictRequest.model_fields.keys())
assert "provider" not in fields, f"PredictRequest 应已删除 provider 字段,现有: {fields}"
def test_predict_request_still_has_core_fields(self):
from src.api.schemas import PredictRequest
fields = set(PredictRequest.model_fields.keys())
for required in ("match_id", "model", "prompt_version", "mode"):
assert required in fields, f"缺少核心字段 {required}"