refactor(notification): 建立通知渠道通用管理契约 channel_manage

- schemas 层新增 NotificationAction 公共动作词汇表(状态/二维码/退出/连通性/缓存迁移)
- 模块实现统一 channel_manage(channel, action, **params),按渠道名路由,
  非本渠道返回 None;动作语义、表单参数解释与临时参数初始化封闭在模块内
- NotificationChain 收敛为单一 manage_channel 纯透明转发,
  chain 层不再出现任何渠道名与模块特色
- endpoint 路由路径保持前端兼容,内部统一走通用接口
- 新增 6 项契约守护测试,文档记录通用模式:新渠道接入无需改 chain
This commit is contained in:
jxxghp
2026-08-16 06:35:46 +08:00
parent a66cbe6192
commit 5b0c631f80
6 changed files with 242 additions and 252 deletions
+120
View File
@@ -0,0 +1,120 @@
"""
通知渠道通用管理契约(channel_manage)守护测试
验证通用模式的三条核心性质:
1. 按渠道名路由:非本渠道直接返回 None,交由分发机制继续执行其它模块
2. 动作词汇表由 schemas 契约层统一定义,未支持动作返回统一错误结构
3. 临时参数初始化封闭在模块内:无已保存配置时可基于表单参数构造临时客户端
"""
from types import SimpleNamespace
import pytest
from app.modules.wechatclawbot import WechatClawBotModule
from app.schemas.types import MessageChannel, NotificationAction
@pytest.fixture
def module():
return WechatClawBotModule()
def test_channel_manage_routes_only_matching_channel(module):
"""非本渠道的管理请求返回 None,run_module 分发将继续执行其它模块。"""
result = module.channel_manage(
channel=MessageChannel.Telegram,
action=NotificationAction.STATUS,
)
assert result is None
def test_channel_manage_rejects_unknown_action(module):
"""动作词汇表之外的请求返回统一错误结构。"""
result = module.channel_manage(
channel=MessageChannel.WechatClawBot,
action="not_an_action",
)
assert result["success"] is False
assert "不支持" in result["message"]
def test_channel_manage_requires_saved_config_without_form_params(module, monkeypatch):
"""无任何配置且未提供表单参数时,返回提示保存配置的错误。"""
monkeypatch.setattr(module, "get_instance", lambda name=None: None)
result = module.channel_manage(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.TEST_CONNECTION,
)
assert result["success"] is False
assert "保存" in result["message"]
def test_channel_manage_builds_temporary_client_from_form_params(module, monkeypatch):
"""无已保存配置时基于表单参数构造临时客户端,实现未保存即预览。"""
monkeypatch.setattr(module, "get_instance", lambda name=None: None)
captured = {}
class _FakeClient:
def test_connection(self):
captured["called"] = True
return False, "未登录,请先扫码完成绑定"
monkeypatch.setattr(
"app.modules.wechatclawbot.WechatClawBot",
lambda **kwargs: (captured.update(kwargs=kwargs), _FakeClient())[1],
)
result = module.channel_manage(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.TEST_CONNECTION,
source="预览渠道",
WECHATCLAWBOT_BASE_URL="http://127.0.0.1:1",
)
assert captured["called"] is True
assert captured["kwargs"]["name"] == "预览渠道"
assert captured["kwargs"]["auto_start_polling"] is False
assert result["success"] is False
assert "未登录" in result["message"]
def test_channel_manage_prefers_saved_instance(module, monkeypatch):
"""存在已保存配置实例时优先使用,不构造临时客户端。"""
saved = SimpleNamespace()
saved.get_status = lambda refresh_remote, auto_generate_qrcode: {
"success": True,
"connected": True,
}
monkeypatch.setattr(module, "get_config", lambda name=None: SimpleNamespace(name="已保存"))
monkeypatch.setattr(module, "get_instance", lambda name=None: saved)
result = module.channel_manage(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.STATUS,
source="已保存",
)
assert result["success"] is True
assert result["connected"] is True
def test_channel_manage_migrate_cache_dispatches_without_client(module, monkeypatch):
"""缓存迁移动作不依赖客户端实例,直接走静态迁移逻辑。"""
def _fake_migrate(old_name, new_name, cleanup_old, overwrite):
assert old_name == "旧名"
assert new_name == "新名"
return True, "迁移成功"
monkeypatch.setattr(
"app.modules.wechatclawbot.WechatClawBot.migrate_cached_state",
staticmethod(_fake_migrate),
)
result = module.channel_manage(
channel=MessageChannel.WechatClawBot,
action=NotificationAction.MIGRATE_CACHE,
old_name="旧名",
new_name="新名",
)
assert result == {"success": True, "message": "迁移成功"}