Files
kefu/wechat_rpa/test_visual_review_visibility.py
T
2026-09-21 10:34:06 +08:00

139 lines
6.7 KiB
Python

"""Synthetic review persistence: no desktop input, network or live queue access."""
import json
from pathlib import Path
import tempfile
import threading
import time
import unittest
from unittest import mock
from wechat_bot import WeChatBot
class VisualReviewVisibilityTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.path = Path(temporary.name) / 'pending.json'
self.fp = b'v' * 40
self.key = self.fp.hex()
self.bot = WeChatBot.__new__(WeChatBot)
self.bot._pending_lock = threading.RLock()
self.bot._pending_reply_path = str(self.path)
self.bot._queue_log_instance = mock.Mock()
self.bot._forget_uncertain_tracking = mock.Mock()
self.bot.send_mode = 'auto'
self.enterContext(mock.patch('review_policy.current_judge_mode', return_value='score_only'))
self.state = {
'created_at': time.time(), 'updated_at': time.time(),
'display_name': '审核测试客户', 'stage': 'manual_review',
'awaiting_review': True, 'review_origin': 'rule',
'review_reason': '命中审核规则「测试规则」',
'staged_user_text': '合成测试问题', 'staged_reply_text': '合成待审草稿',
'reply_text': '合成待审草稿', 'judge_risk': 'high', 'judge_mode': 'score_only',
'judge_reason': '合成裁判原因',
}
self.bot._pending_reply_sessions = {self.key: self.state}
for function in ('press', 'hotkey', 'click', 'write'):
patcher = mock.patch('wechat_bot.pyautogui.' + function,
side_effect=AssertionError('No real desktop action'))
patcher.start()
self.addCleanup(patcher.stop)
def restart(self):
self.assertTrue(self.bot._persist_pending_replies())
self.bot._pending_reply_sessions = self.bot._load_pending_replies()
self.state = self.bot._pending_reply_sessions[self.key]
return self.state
def test_rule_reason_and_judge_survive_restart_and_auto_mode(self):
restored = self.restart()
self.assertEqual(restored['review_reason'], '命中审核规则「测试规则」')
self.assertEqual(restored['judge_mode'], 'score_only')
self.bot._drop_stale_review_flags()
self.assertTrue(restored['awaiting_review'])
self.assertEqual(restored['review_reason'], '模型裁判评估为高风险')
self.assertEqual(restored['judge_risk'], 'high')
self.assertEqual(restored['judge_reason'], '合成裁判原因')
self.assertEqual(restored['staged_user_text'], '合成测试问题')
self.assertEqual(restored['staged_reply_text'], '合成待审草稿')
def test_review_without_unread_badge_does_not_expire_during_downtime(self):
self.state['created_at'] = self.state['updated_at'] = time.time() - 3 * 86400
self.state['confirmed_unread'] = False
self.assertTrue(self.restart()['awaiting_review'])
def test_legacy_review_with_lost_reason_stays_actionable(self):
self.state.pop('review_reason')
self.state.pop('review_origin')
self.state['stage_detail'] = 'AI 回复已填入输入框,等待人工核对并发送'
self.path.write_text(json.dumps({self.key: self.state}), encoding='utf-8')
self.bot._pending_reply_sessions = self.bot._load_pending_replies()
restored = self.bot._pending_reply_sessions[self.key]
self.bot._drop_stale_review_flags()
self.assertTrue(restored['awaiting_review'])
self.assertIn('历史审核原因', restored['review_reason'])
def test_legacy_review_recovers_rule_reason_from_stage_detail(self):
self.state.pop('review_reason')
self.state.pop('review_origin')
self.state['stage_detail'] = '命中审核规则「测试规则」,已停止自动发送'
self.assertIn('测试规则', self.restart()['review_reason'])
def test_legacy_manual_review_stage_without_boolean_is_restored(self):
self.state.pop('awaiting_review')
restored = self.restart()
self.assertTrue(restored['awaiting_review'])
self.assertEqual(restored['stage'], 'manual_review')
def test_known_global_review_still_honors_explicit_mode_change(self):
self.state.update(review_origin='global', review_reason='', judge_risk='')
restored = self.restart()
self.bot._drop_stale_review_flags()
self.assertFalse(restored.get('awaiting_review'))
def test_explicit_approval_survives_restart_but_is_consumed_on_next_turn(self):
result = self.bot.approve_pending_replies([self.key])
self.assertEqual(result['approved'], [self.key])
restored = self.restart()
self.assertFalse(restored['awaiting_review'])
self.assertTrue(restored['review_approved'])
self.assertGreater(restored['review_approved_at'], 0)
self.assertEqual(restored['stage'], 'queued')
self.bot._reset_pending_batch(self.fp)
self.assertFalse(restored.get('review_approved'))
self.assertNotIn('judge_risk', restored)
self.assertNotIn('review_origin', restored)
def test_next_customer_turn_does_not_inherit_previous_judge_risk(self):
self.bot._reset_pending_batch(self.fp)
with mock.patch('ai_config.AI_REVIEW_RULES', []):
self.assertEqual(self.bot._reply_needs_review(self.state, '新问题', '新回答'), (False, ''))
def test_review_outcome_preserves_specific_reason_for_desktop_and_log(self):
self.bot._log_send_outcome(self.fp, False, '合成待审草稿')
self.assertIn('测试规则', self.state['stage_detail'])
self.assertEqual(self.state['stage'], 'manual_review')
event = self.bot._queue_log_instance.append.call_args
self.assertIn('测试规则', event.args[-1])
def test_user_can_cancel_restored_review_without_sending(self):
self.restart()
result = self.bot.cancel_pending_replies([self.key])
self.assertEqual(result['deleted'], [self.key])
self.assertNotIn(self.key, self.bot._pending_reply_sessions)
self.assertEqual(json.loads(self.path.read_text(encoding='utf-8')), {})
def test_failed_approval_save_keeps_review_visible_and_unapproved(self):
with mock.patch.object(self.bot, '_persist_pending_replies_unlocked', return_value=False):
result = self.bot.approve_pending_replies([self.key])
self.assertFalse(result['approved'])
self.assertIn('error', result)
restored = self.bot._pending_reply_sessions[self.key]
self.assertTrue(restored['awaiting_review'])
self.assertFalse(restored.get('review_approved'))
if __name__ == '__main__':
unittest.main()