Files
helper/tests/test_text_routing.py
2026-10-15 18:27:46 +08:00

128 lines
6.1 KiB
Python

"""好友/群消息路由和实际桥接 ABI 的回归验证,不向微信发送消息。"""
from __future__ import annotations
import ctypes
from pathlib import Path
import sys
import time
import unittest
from wxbot_framework.sdk import Message
from wxbot_framework.text import TextRouter, validate_target
ROOT = Path(__file__).resolve().parent.parent
NATIVE = ROOT / "native"
SELF = "wxid_framework_self"
FRIEND = "wxid_framework_friend"
GROUP = "12345678901234567890@chatroom"
class TextRoutingTests(unittest.TestCase):
def setUp(self):
self.router = TextRouter()
def identify(self):
return self.router.convert(SELF, "filehelper", "init")
def test_private_chat_requires_account_and_keeps_actual_sender(self):
self.assertIsNone(self.router.convert(FRIEND, SELF, "未识别"))
self.assertEqual(self.router.unidentified, 1)
self.identify()
data = self.router.convert(FRIEND, SELF, "echo 好友 🌸\n第二行", sequence=3)
message = Message.from_native(11046, data, "session")
self.assertEqual((message.chat_id, message.sender_id, message.receiver_id), (FRIEND, FRIEND, SELF))
self.assertFalse(message.is_group)
self.assertFalse(message.is_self)
self.assertEqual(message.text, "echo 好友 🌸\n第二行")
def test_group_identifies_account_and_separates_author_body_and_chat(self):
data = self.router.convert(GROUP, SELF, FRIEND + ":\necho 群聊 🌸\n二:\n三", sequence=9)
message = Message.from_native(11046, data, "session")
self.assertEqual(self.router.self_wxid, SELF)
self.assertEqual((message.chat_id, message.sender_id), (GROUP, FRIEND))
self.assertEqual(message.text, "echo 群聊 🌸\n二:\n三")
self.assertTrue(message.is_group)
self.assertEqual(data["from_wxid"], FRIEND)
self.assertEqual(data["room_wxid"], GROUP)
def test_phone_sent_private_and_group_messages_do_not_create_reply_loops(self):
self.identify()
self.assertIsNone(self.router.convert(SELF, FRIEND, "echo 私聊同步"))
self.assertIsNone(self.router.convert(SELF, GROUP, "echo 群同步"))
self.assertIsNone(self.router.convert(GROUP, SELF, SELF + ":\necho 自己的群消息"))
self.assertEqual(self.router.ignored_self, 3)
# 文件传输助手仍允许手机自己发送测试指令。
self.assertTrue(self.identify()["is_self"])
def test_unrecognized_group_author_is_not_guessed(self):
self.assertIsNone(self.router.convert(GROUP, SELF, "没有成员前缀"))
self.assertIsNone(self.router.convert(GROUP, SELF, "普通内容:\n正文"))
self.assertEqual(self.router.unparsed_groups, 2)
def test_primary_phone_can_control_and_play_group_game(self):
self.identify()
for text in ("/help", "成语接龙", "合并接龙", "退出接龙", "意气风发"):
data = self.router.convert(GROUP, SELF, SELF + ":\n" + text)
self.assertTrue(data["is_self"])
self.assertEqual(data["msg"], text)
def test_native_service_skips_recent_bot_text_but_allows_players(self):
if str(NATIVE) not in sys.path:
sys.path.insert(0, str(NATIVE))
from text_service import NativeTextService
from filehelper_service import ReceivedMessage, ReceivedTextMessage
service = NativeTextService(pid=123)
service.router.set_account(SELF)
service._sent_texts.append((GROUP, "意气风发", time.monotonic()))
own = ReceivedMessage(type=1, sender=GROUP.encode(), receiver=SELF.encode(),
text=(SELF + ":\n意气风发").encode("utf-8"))
mapped = service._map_message(own)
self.assertTrue(mapped["bot_generated"])
self.assertFalse(mapped["dispatch_to_plugins"])
own.text = (SELF + ":\n发扬光大").encode("utf-8")
self.assertEqual(service._map_message(own)["msg"], "发扬光大")
own.text = (FRIEND + ":\n意气风发").encode("utf-8")
self.assertEqual(service._map_message(own)["sender_id"], FRIEND)
def test_account_change_stops_old_connection(self):
self.identify()
with self.assertRaises(RuntimeError):
self.router.convert("wxid_another_account", "filehelper", "init")
self.assertEqual(self.router.self_wxid, SELF)
def test_public_and_system_peers_are_not_dispatched(self):
self.identify()
self.assertIsNone(self.router.convert("gh_example", SELF, "echo 公众号"))
self.assertIsNone(self.router.convert("weixin", SELF, "echo 系统"))
def test_target_validation_matches_native_buffer(self):
for target in ("filehelper", FRIEND, GROUP, "a" * 127):
self.assertEqual(validate_target(target), target)
for target in ("", "a" * 128, "好友名称", "wxid_a\0b", "a b", None):
with self.assertRaises(ValueError):
validate_target(target)
@unittest.skipUnless(sys.platform == "win32", "本机 Windows ABI 检查")
def test_compiled_bridge_exports_and_wire_structures(self):
if str(NATIVE) not in sys.path:
sys.path.insert(0, str(NATIVE))
import pefile
from run_selftest import Config, TextConfig, AtTextConfig, TEXT_DLL
from filehelper_service import ReceivedMessage, ReceivedTextMessage
self.assertEqual(ctypes.sizeof(Config), 1560)
self.assertEqual(ctypes.sizeof(TextConfig), 1688)
self.assertEqual(TextConfig.target.offset, 1560)
self.assertEqual(ctypes.sizeof(AtTextConfig), 2712)
self.assertEqual(AtTextConfig.members.offset, 1688)
self.assertEqual(ctypes.sizeof(ReceivedMessage), 1312)
self.assertEqual(ctypes.sizeof(ReceivedTextMessage), 5408)
with pefile.PE(str(TEXT_DLL)) as pe:
exports = {item.name for item in pe.DIRECTORY_ENTRY_EXPORT.symbols}
self.assertEqual(pe.FILE_HEADER.Machine, 0x8664)
self.assertTrue({b"WxSendText", b"WxSendAtText", b"WxStartTextReceiver", b"WxPollTextMessage", b"WxPollTextMessageV2",
b"WxWaitForTasks", b"WxRemoveSelfTest"}.issubset(exports))
if __name__ == "__main__":
unittest.main()