224 lines
7.0 KiB
Python
224 lines
7.0 KiB
Python
"""LLM 翻译节点测试。
|
|
|
|
覆盖真实 HTTP 服务调用、超时配置、SRT 回填、异常与入口点启动等路径。
|
|
"""
|
|
|
|
import json
|
|
import runpy
|
|
import threading
|
|
import urllib.error
|
|
import urllib.request
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
from pathlib import Path
|
|
|
|
from wov_node_llm.__main__ import invoke, translate_lines
|
|
from wov_sdk.models import InvokeRequest
|
|
|
|
|
|
class FakeUrlOpenResponse:
|
|
"""模拟 urllib 响应对象,提供固定 LLM 译文内容。"""
|
|
|
|
def __init__(self, content: str) -> None:
|
|
# 预编码为 Chat Completions 风格的 JSON 响应体。
|
|
self._payload = json.dumps(
|
|
{"choices": [{"message": {"content": content}}]}
|
|
).encode("utf-8")
|
|
|
|
def read(self) -> bytes:
|
|
return self._payload
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args) -> bool:
|
|
return False
|
|
|
|
|
|
def _make_srt(tmp_path, count=5) -> Path:
|
|
"""生成标准 SRT 测试文件,文本行为“原文字幕N”。"""
|
|
lines = []
|
|
for index in range(count):
|
|
lines.extend(
|
|
[
|
|
str(index + 1),
|
|
f"00:00:{index:02d},000 --> 00:00:{index + 1:02d},000",
|
|
f"原文字幕{index + 1}",
|
|
"",
|
|
]
|
|
)
|
|
path = tmp_path / "in.srt"
|
|
path.write_text("\n".join(lines), encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def test_translate_lines_via_fake_api(monkeypatch) -> None:
|
|
"""验证通过真实 HTTP 服务器调用 LLM 接口并保持行顺序。"""
|
|
class Handler(BaseHTTPRequestHandler):
|
|
def do_POST(self) -> None:
|
|
length = int(self.headers.get("Content-Length", "0"))
|
|
self.rfile.read(length)
|
|
body = json.dumps(
|
|
{
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"content": "译文一\n译文二\n译文三\n译文四\n译文五"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
).encode("utf-8")
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.end_headers()
|
|
self.wfile.write(body)
|
|
|
|
def log_message(self, format, *args) -> None:
|
|
return
|
|
|
|
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
|
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
monkeypatch.setenv(
|
|
"LLM_API_BASE",
|
|
f"http://127.0.0.1:{server.server_address[1]}/v1/chat/completions",
|
|
)
|
|
monkeypatch.setenv("LLM_API_KEY", "test-key")
|
|
result = translate_lines(
|
|
["一", "二", "三", "四", "五"],
|
|
{"target_language": "zh-CN"},
|
|
)
|
|
assert result == ["译文一", "译文二", "译文三", "译文四", "译文五"]
|
|
finally:
|
|
server.shutdown()
|
|
server.server_close()
|
|
thread.join(timeout=5)
|
|
|
|
|
|
def test_translate_lines_api_error(monkeypatch) -> None:
|
|
"""验证 LLM 接口不可用时抛出 URLError。"""
|
|
def fail_open(request, timeout):
|
|
raise urllib.error.URLError("api down")
|
|
|
|
monkeypatch.setattr("wov_node_llm.__main__.urllib.request.urlopen", fail_open)
|
|
try:
|
|
translate_lines(["一"], {})
|
|
raise AssertionError("expected failure")
|
|
except urllib.error.URLError:
|
|
pass
|
|
|
|
|
|
def test_translate_lines_default_timeout(monkeypatch) -> None:
|
|
"""验证未配置超时时使用默认 600 秒。"""
|
|
captured = {}
|
|
|
|
def fake_open(request, timeout):
|
|
captured["timeout"] = timeout
|
|
return FakeUrlOpenResponse("译文一")
|
|
|
|
monkeypatch.setattr("wov_node_llm.__main__.urllib.request.urlopen", fake_open)
|
|
monkeypatch.setenv("LLM_API_BASE", "http://fake/v1/chat/completions")
|
|
|
|
result = translate_lines(["一"], {})
|
|
|
|
assert result == ["译文一"]
|
|
assert captured["timeout"] == 600
|
|
|
|
|
|
def test_translate_lines_env_timeout(monkeypatch) -> None:
|
|
"""验证 LLM_TIMEOUT_SECONDS 环境变量可覆盖超时。"""
|
|
captured = {}
|
|
|
|
def fake_open(request, timeout):
|
|
captured["timeout"] = timeout
|
|
return FakeUrlOpenResponse("译文一")
|
|
|
|
monkeypatch.setattr("wov_node_llm.__main__.urllib.request.urlopen", fake_open)
|
|
monkeypatch.setenv("LLM_API_BASE", "http://fake/v1/chat/completions")
|
|
monkeypatch.setenv("LLM_TIMEOUT_SECONDS", "45")
|
|
|
|
translate_lines(["一"], {})
|
|
|
|
assert captured["timeout"] == 45
|
|
|
|
|
|
def test_invoke_success(tmp_path, monkeypatch) -> None:
|
|
"""验证成功调用会把译文回填到 SRT 并输出 cn.srt。"""
|
|
source = _make_srt(tmp_path)
|
|
|
|
def fake_translate(lines, params):
|
|
return [f"译文{i + 1}" for i in range(len(lines))]
|
|
|
|
monkeypatch.setattr("wov_node_llm.__main__.translate_lines", fake_translate)
|
|
response = invoke(
|
|
InvokeRequest(
|
|
run_id="run_1",
|
|
node_instance_id="ni_1",
|
|
inputs={"srt_uri": str(source)},
|
|
params={"target_language": "zh-CN"},
|
|
output_dir=str(tmp_path / "out"),
|
|
)
|
|
)
|
|
assert response.status == "completed"
|
|
content = Path(response.outputs["cn_srt_uri"]).read_text(encoding="utf-8")
|
|
assert "译文1" in content
|
|
|
|
|
|
def test_invoke_pads_short_translation(tmp_path, monkeypatch) -> None:
|
|
"""验证译文行数不足时用空行补齐,保持 SRT 结构完整。"""
|
|
source = _make_srt(tmp_path, count=3)
|
|
monkeypatch.setattr(
|
|
"wov_node_llm.__main__.translate_lines",
|
|
lambda lines, params: ["only one"],
|
|
)
|
|
response = invoke(
|
|
InvokeRequest(
|
|
run_id="run_2",
|
|
node_instance_id="ni_2",
|
|
inputs={"srt_uri": str(source)},
|
|
output_dir=str(tmp_path / "out2"),
|
|
)
|
|
)
|
|
assert response.status == "completed"
|
|
|
|
|
|
def test_invoke_missing_input(tmp_path) -> None:
|
|
"""验证缺少 srt_uri 时返回失败。"""
|
|
response = invoke(
|
|
InvokeRequest(
|
|
run_id="run_3",
|
|
node_instance_id="ni_3",
|
|
inputs={},
|
|
output_dir=str(tmp_path),
|
|
)
|
|
)
|
|
assert response.status == "failed"
|
|
|
|
|
|
def test_invoke_missing_file(tmp_path) -> None:
|
|
"""验证 SRT 文件不存在时返回失败。"""
|
|
response = invoke(
|
|
InvokeRequest(
|
|
run_id="run_4",
|
|
node_instance_id="ni_4",
|
|
inputs={"srt_uri": str(tmp_path / "missing.srt")},
|
|
output_dir=str(tmp_path),
|
|
)
|
|
)
|
|
assert response.status == "failed"
|
|
|
|
|
|
def test_entrypoint(monkeypatch) -> None:
|
|
"""验证 python -m wov_node_llm 会加载 llm-translate manifest。"""
|
|
module_path = Path(__file__).resolve().parent.parent / "wov_node_llm" / "__main__.py"
|
|
captured = {}
|
|
|
|
def fake_run_node(manifest, handler) -> None:
|
|
captured["id"] = manifest.id
|
|
|
|
monkeypatch.setattr("wov_sdk.server.run_node", fake_run_node)
|
|
runpy.run_path(str(module_path), run_name="__main__")
|
|
assert captured["id"] == "llm-translate"
|