Files
vrsub/tests/shared/llm_service.py
T
cat-shark 8a715a8064 test: 按模块重写测试代码,删除旧平铺结构
按"测试规则"重写 tests/:一个模块一个目录、用例按数据→过程→验证三段书写、
不保留全局 conftest.py、测试过程只调用真实生产代码。

结构(73 个文件、30 个模块目录、477 用例):
- tests/nodes/  15 个模块目录(srt/whisper/ass/ffmpeg/frame_extract/vlm/
  subtitle_ocr/llm/llm_filter/subtitle_cleanup/subtitle_correction/
  proper_nouns/adaptive_pool/vad_profiler/echo);
- tests/app/    11 个模块目录(db/scheduler/batch/maintenance/registry/seed/
  storage/config/logging/main/routers 三组 API);
- tests/sdk/test_models、tests/web/test_crop、tests/shared(公共设施)。

测试数据随模块目录入库(tests/**/data/),删除根级 testdata/;.gitignore
的 data/ 改为 /data/,否则会连带忽略 tests/**/data/ 导致测试数据无法入库。

顺带发现并修复三个真实缺陷:
- nodes/srt.py:相邻条目缺少空行时把下一条时间轴吞进正文(静默错位),
  改为正文行遇时间戳行即报错;
- src/wov_app/scheduler.py:_file_size 只捕获 OSError,含 \x00 的产物 URI
  抛 ValueError 导致任务误判失败,改为同时捕获;
- nodes/subtitle_correction.py:生产代码依赖测试包解析 SRT,
  改用生产模块 nodes/srt.py。

真实模型/服务集成测试按外部状态跳过:新增 tests/shared/gpu_memory.py
(运行时探测显存、CUDA OOM 转跳过)与 tests/shared/llm_service.py
(无 Key / 余额 / 限流转跳过)。全量 477 passed。
2026-09-13 15:40:56 +08:00

84 lines
3.4 KiB
Python
Raw 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.
"""真实 LLM 服务的可用性判定(供真实 LLM 集成测试共用)。
真实 LLM 集成测试有两个外部前提:配置了 `LLM_API_KEY`、且服务端可用。
两者都属于**外部环境状态**(密钥、余额、配额、网络),不是被测代码的行为;
按测试规则,这类外部状态缺失时应跳过而不是把缺陷计到代码头上。
本模块把该判定收敛到一处,避免各测试各自实现、语义漂移:
- `require_llm_credentials()`:无 Key 时直接跳过;
- `skip_on_service_unavailable()`:把鉴权/余额/限流类 HTTP 错误(401/402/403/429
转成跳过(附带原因),其余错误(如 5xx、网络异常、返回结构错误)照常失败,
以保证真实回归仍能被发现。
"""
from __future__ import annotations
import os
import urllib.error
from contextlib import contextmanager
import pytest
# 视为"服务端不可用/账号不可用"的 HTTP 状态码:
# 401 未授权、402 需要付费(余额/额度耗尽)、403 禁止访问、429 限流。
_UNAVAILABLE_CODES = frozenset({401, 402, 403, 429})
def require_llm_credentials() -> None:
"""未配置 LLM_API_KEY 时跳过真实 LLM 集成测试。"""
if not os.getenv("LLM_API_KEY"):
pytest.skip("未配置 LLM_API_KEY,跳过真实 LLM 集成测试")
@contextmanager
def skip_on_service_unavailable():
"""执行真实 LLM 调用;鉴权/余额/限流类错误转为跳过,其余错误向上抛出。
这样既不会因账户余额或临时限流把测试套件判红(外部状态问题),
也不会掩盖真正的回归(返回结构错误、代码异常仍会失败)。
"""
try:
yield
except urllib.error.HTTPError as exc:
if exc.code in _UNAVAILABLE_CODES:
pytest.skip(f"LLM 服务/账号当前不可用(HTTP {exc.code}),跳过真实集成测试")
raise
def probe_llm_or_skip(model: str | None = None) -> None:
"""向真实 LLM 接口发一次最小请求,服务/账号不可用时跳过测试。
用途:被测函数(如 `nodes/subtitle_correction.correct_entry`)出于生产
需要会把调用异常吞掉并返回空串,测试因此无法从返回值区分"服务不可用"
与"模型没有泛化"。此探针在断言之前把外部状态问题显式暴露出来并跳过,
使断言只针对真实的能力回归。
探测失败判定与 `skip_on_service_unavailable` 一致(401/402/403/429 跳过);
其余错误(5xx、网络、返回结构异常)向上抛出,仍视为需要修复的问题。
"""
import json as _json
import urllib.request
require_llm_credentials()
api_base = os.getenv("LLM_API_BASE", "https://api.siliconflow.cn/v1/chat/completions")
api_key = os.getenv("LLM_API_KEY", "")
body = {
"model": model or os.getenv("LLM_MODEL", "Qwen/Qwen3.5-35B-A3B"),
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 1,
"enable_thinking": False,
}
request = urllib.request.Request(
api_base,
data=_json.dumps(body).encode("utf-8"),
headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
method="POST",
)
with skip_on_service_unavailable():
try:
with urllib.request.urlopen(request, timeout=30) as response:
_json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError:
raise