Files
HeXi/tests/test_video_sender_link.py
sansenhoshiandClaude Code 574216645c feat(video-analysis): 本地直发成功且群策略要下载链接时补传 S3
本地直发经 hexi/core/outbound_media 钩子改写后已真正可用,此前它因协议端不在本机
恒失败,public_url 因此总有值(每次实际都落到第 2 级 S3)。本地通道通了以后若不补,
public_url 会恒为 None,entry.py 的 `if pub and policy.sends_link` 会让「发送下载
链接」静默失效。

- _local_sent / _public_link_for 把「取链接」与「发送」解耦:只有策略真要链接
  (policy.sends_link)时才补一次 S3 上传,其余情况一概不碰 S3 —— 媒体不再进
  S3 归档是有意的,temp 保留交给 cleanup
- 补传失败只记日志返回 None:发送本身已经成功,不能反过来判失败
- tests/test_video_sender_link.py:覆盖 sends_link 开/关、多文件逐个补传、
  拿不到 public_url、上传抛异常、policy 为 None 五条路径

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 18:47:12 +08:00

156 lines
5.4 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.
"""媒体发送成功后「补传 S3 取下载链接」的单元测试
背景:2026-09-22 起 `hexi/core/outbound_media` 的全局钩子把本地路径改写成
`/media/<token>`,本地直发那一级才真正可用。此前它因协议端不在本机、`file://`
必然 ENOENT 而**恒失败**,所以每次都落到第 2 级 S3 —— `public_url` 因此总有值。
本地直发通了以后,如果什么都不做,`public_url` 会恒为 None,`entry.py` 的
`if pub and policy.sends_link` 就会让「发送下载链接」**静默失效**。
`_local_sent` / `_public_link_for` 就是为此把「取链接」与「发送」解耦:
只有策略真要链接(`sends_link`)时才补一次 S3,其余情况一概不碰 S3。
sender.py 有插件内相对导入(`..config` / `..policy` / `..services...`),
无法裸加载,故在 sys.modules 里预置这些依赖的替身再按路径加载目标模块。
"""
import importlib.util
import sys
import types
from pathlib import Path
import pytest
_REPO = Path(__file__).resolve().parents[1]
_PKG = "hexi.plugins.nonebot_plugin_video_analysis"
_SENDER_PATH = (
_REPO
/ "hexi"
/ "plugins"
/ "nonebot_plugin_video_analysis"
/ "handlers"
/ "sender.py"
)
#: 预置的替身模块名 → 该模块要暴露的属性占位(真实函数在测试里替换)
_STUBS = {
_PKG: {},
f"{_PKG}.handlers": {},
f"{_PKG}.config": {"group_file_settings": lambda: ([], None)},
f"{_PKG}.policy": {"Policy": type("Policy", (), {})},
f"{_PKG}.services": {},
f"{_PKG}.services.storage": {},
f"{_PKG}.services.storage.group_file": {"upload_group_files": None},
f"{_PKG}.services.storage.s3": {"upload_with_plan": None},
f"{_PKG}.utils": {"media_rel_dir_of": lambda _p: ""},
}
@pytest.fixture(scope="module")
def sender():
saved = {name: sys.modules.get(name) for name in _STUBS}
for name, attrs in _STUBS.items():
module = types.ModuleType(name)
for key, value in attrs.items():
setattr(module, key, value)
sys.modules[name] = module
target = f"{_PKG}.handlers.sender"
saved[target] = sys.modules.get(target)
spec = importlib.util.spec_from_file_location(target, _SENDER_PATH)
module = importlib.util.module_from_spec(spec)
sys.modules[target] = module
spec.loader.exec_module(module)
yield module
for name, previous in saved.items():
if previous is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = previous
class _Policy:
"""只带 sends_link 的策略替身(真实 Policy 需要一堆字段/归一化)。"""
def __init__(self, sends_link: bool):
self.sends_link = sends_link
@pytest.fixture
def uploads(monkeypatch, sender):
"""记录 upload_with_plan 的调用;返回 (调用列表, 设置返回值的钩子)。"""
calls: list[Path] = []
result: dict = {"pub": "https://pub.example/x.mp4", "raise": None}
def fake_upload(file_path, *, policy=None):
calls.append(Path(file_path))
if result["raise"]:
raise result["raise"]
return "local-url", result["pub"]
monkeypatch.setattr(sender, "upload_with_plan", fake_upload)
return calls, result
@pytest.fixture
def media(sender):
def build(sends_link: bool, n: int = 1, policy=None):
files = [Path(f"D:/temp/f{i}.mp4") for i in range(n)]
return sender.PendingMedia(
files=files,
policy=_Policy(sends_link) if policy is None else policy,
)
return build
async def test_no_s3_when_link_disabled(sender, media, uploads):
"""默认路径:本地发成功就够了,绝不碰 S3(媒体不走 S3 归档是有意的)。"""
calls, _ = uploads
ok, pub = await sender._local_sent(media(sends_link=False))
assert (ok, pub) == (True, None)
assert calls == []
async def test_s3_fetched_when_link_enabled(sender, media, uploads):
"""策略要下载链接时:补传 S3 并把 public_url 交出去(本次修的回归)。"""
calls, _ = uploads
ok, pub = await sender._local_sent(media(sends_link=True))
assert (ok, pub) == (True, "https://pub.example/x.mp4")
assert len(calls) == 1
async def test_multi_file_uploads_each(sender, media, uploads):
calls, _ = uploads
ok, pub = await sender._local_sent(media(sends_link=True, n=3))
assert ok is True
assert pub == "https://pub.example/x.mp4"
assert len(calls) == 3
async def test_no_public_url_keeps_none(sender, media, uploads):
"""S3 有本地链接但没公网(upload_public 关)→ 不带链接,但也不算失败。"""
calls, result = uploads
result["pub"] = None
ok, pub = await sender._local_sent(media(sends_link=True))
assert (ok, pub) == (True, None)
assert len(calls) == 1
async def test_upload_failure_never_breaks_send(sender, media, uploads):
"""补传失败只记日志:发送本身已经成功了,不能反过来判失败。"""
calls, result = uploads
result["raise"] = RuntimeError("S3 挂了")
ok, pub = await sender._local_sent(media(sends_link=True))
assert (ok, pub) == (True, None)
assert len(calls) == 1
async def test_policy_none_is_safe(sender, media, uploads):
"""无策略(如 Web 面板路径)时不补传也不炸。"""
calls, _ = uploads
ok, pub = await sender._local_sent(media(sends_link=False, policy=None))
assert (ok, pub) == (True, None)
assert calls == []