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

183 lines
7.6 KiB
Python

"""验证独立接龙、合并首答和发送失败时的状态,使用内存上下文。"""
import logging
import time
import unittest
from contextlib import nullcontext
from unittest.mock import patch
from plugins.idiom_chain import Plugin, HELP
from wxbot_framework.llm import LLMError
from wxbot_framework.sdk import Message
from wxbot_framework.personality import PersonalityState
ROOM = "12345678@chatroom"
A, B = "wxid_player_a", "wxid_player_b"
class JSONClient:
def __init__(self, result):
self.result = result
def complete_json(self, system, user):
return self.result
class Context:
def __init__(self):
self.config = {"member_names": {ROOM: {A: "小明"}}}
self.llm = JSONClient({"idiom": "一心一意"})
self.logger = logging.getLogger("IdiomTests")
self.sent = []
self.succeeds = True
def personality(self, message):
return PersonalityState(message.chat_id)
def llm_for(self, message, *, personality=None):
return self.llm
def personality_guard(self, state):
return nullcontext()
def reply(self, message, text, *, delivery_guard=None):
self.sent.append((message.chat_id, text, None, None))
return self.succeeds
def reply_at(self, message, text, *, display_name=None, delivery_guard=None):
self.sent.append((message.chat_id, text, message.sender_id, display_name))
return self.succeeds
class IdiomPluginTests(unittest.TestCase):
def setUp(self):
self.ctx = Context()
self.plugin = Plugin()
self.plugin.on_load(self.ctx)
def send(self, text, sender=A, room=ROOM, *, session="one", received_at=None, is_self=False, is_group=True):
message = Message(text, room, sender_id=sender, session_id=session, is_group=is_group,
is_self=is_self, received_at=time.monotonic() if received_at is None else received_at)
self.plugin.on_message(message, self.ctx)
return message
def start(self):
self.send("成语接龙")
self.ctx.llm.result = {"is_idiom": True, "reason": "常见成语", "next_idiom": "发扬光大"}
def test_two_players_are_independent_and_use_member_names(self):
self.start()
# 两个答案都在机器人第一次回复之前到达:B 仍应从开场词接。
b_received = time.monotonic()
self.send("意气风发", A)
self.send("意气风发", B, received_at=b_received)
game = self.plugin.games[ROOM]
self.assertEqual(game.players[A].current, "发扬光大")
self.assertEqual(game.players[B].current, "发扬光大")
self.assertEqual(self.ctx.sent[-2][2:], (A, "小明"))
self.assertEqual(self.ctx.sent[-1][2:], (B, None))
self.ctx.llm.result["next_idiom"] = "方兴未艾"
self.send("大大方方", A)
self.assertEqual(game.players[A].current, "方兴未艾")
self.assertEqual(game.players[B].current, "发扬光大")
def test_groups_do_not_share_game(self):
self.start()
self.send("意气风发", room="87654321@chatroom")
self.assertEqual(len(self.ctx.sent), 1)
def test_bad_answer_keeps_current_question(self):
self.start()
self.send("异想天开")
self.assertEqual(self.plugin.games[ROOM].players[A].current, "一心一意")
self.assertIn("接龙失败", self.ctx.sent[-1][1])
def test_merged_mode_uses_latest_word_and_only_first_received_answer(self):
self.start()
self.send("意气风发")
self.send("合并接龙")
game = self.plugin.games[ROOM]
self.assertEqual(game.shared.current, "发扬光大")
self.assertEqual(game.players, {})
arrived = time.monotonic()
self.ctx.llm.result["next_idiom"] = "方兴未艾"
self.send("大大方方", A, received_at=arrived)
count = len(self.ctx.sent)
self.send("大大方方", B, received_at=arrived + 0.00000001)
self.assertEqual(len(self.ctx.sent), count)
self.assertIsNone(self.ctx.sent[-1][2])
self.assertEqual(game.shared.current, "方兴未艾")
def test_failed_merged_answer_still_consumes_round(self):
self.start()
self.send("合并接龙")
arrived = time.monotonic()
self.send("异想天开", received_at=arrived)
count = len(self.ctx.sent)
self.send("意气风发", B, received_at=arrived)
self.assertEqual(len(self.ctx.sent), count)
self.assertEqual(self.plugin.games[ROOM].shared.current, "一心一意")
self.send("意气风发", B)
self.assertEqual(self.plugin.games[ROOM].shared.current, "发扬光大")
def test_exit_and_restart(self):
self.start()
self.send("退出接龙")
self.assertNotIn(ROOM, self.plugin.games)
count = len(self.ctx.sent)
self.send("意气风发")
self.assertEqual(len(self.ctx.sent), count)
self.ctx.llm.result = {"idiom": "一心一意"}
self.send("成语接龙")
self.assertIn(ROOM, self.plugin.games)
def test_help_and_primary_phone_commands(self):
self.send("/help", is_self=True)
self.assertEqual(self.ctx.sent[-1][1], HELP)
self.assertEqual(HELP, "1. 成语接龙\n\n2. /绑定 昵称\n\n3. /总结 昵称\n\n4. /总结群聊\n\n5. /切换人格 嘴臭|卑微|夸夸|取消\n\n6. 艾特机器人对话\n\n7. /清空\n\n8. /help")
self.assertLessEqual(len(HELP.encode("utf-8")), 1023)
self.send("成语接龙", is_self=True)
self.assertEqual(self.ctx.sent[-1][1], "一心一意")
self.assertIn(ROOM, self.plugin.games)
self.ctx.llm.result = {"is_idiom": True, "reason": "常见成语", "next_idiom": "发扬光大"}
self.send("意气风发", is_self=True)
self.assertEqual(self.plugin.games[ROOM].players[A].current, "发扬光大")
def test_own_long_reply_does_not_create_loop_and_private_is_ignored(self):
self.start()
count = len(self.ctx.sent)
self.send("接龙成功!一心一意 → 意气风发", is_self=True)
self.send("/help", is_group=False)
self.assertEqual(len(self.ctx.sent), count)
def test_reconnect_resets_game(self):
self.start()
self.send("意气风发", session="two")
self.assertEqual(self.plugin.games, {})
def test_failed_send_does_not_advance_question(self):
self.start()
self.ctx.succeeds = False
self.send("意气风发")
self.assertEqual(self.plugin.games[ROOM].players, {})
self.send("合并接龙")
self.assertEqual(self.plugin.games[ROOM].mode, "independent")
def test_model_failure_keeps_question(self):
self.start()
with patch("plugins.idiom_chain.IdiomJudge.evaluate", side_effect=LLMError("测试超时")), self.assertLogs("IdiomTests"):
self.send("意气风发")
self.assertEqual(self.plugin.games[ROOM].players[A].current, "一心一意")
self.assertIn("重试", self.ctx.sent[-1][1])
def test_model_opening_failure_does_not_create_game(self):
with patch("plugins.idiom_chain.IdiomJudge.opening", side_effect=LLMError("测试超时")), self.assertLogs("IdiomTests"):
self.send("成语接龙")
self.assertEqual(self.plugin.games, {})
def test_invalid_member_name_configuration(self):
for options in ({"member_names": []}, {"member_names": {ROOM: {A: "\n"}}},
{"member_names": {ROOM: {A: "A" * 65}}}):
self.ctx.config = options
with self.subTest(options=options), self.assertRaises(ValueError):
Plugin().on_load(self.ctx)