154 lines
7.3 KiB
Python
154 lines
7.3 KiB
Python
"""Synthetic protocol review workflow: local temp state, no network or WeCom I/O."""
|
|
import json
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from protocol_engine import ProtocolBot, session_key, MANUAL_MEDIA_REASON, MANUAL_BATCH_REASON
|
|
from test_native_protocol_pipeline import Database, Sender, message, ACCOUNT, CONV
|
|
from wecom_native_sender import DeliveryUnknown
|
|
|
|
|
|
class ProtocolReviewVisibilityTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
self.db = Database()
|
|
self.sender = Sender(self.db)
|
|
self.generate = mock.Mock(return_value='合成回复草稿')
|
|
for patcher in (
|
|
mock.patch('protocol_engine.application_data_dir', return_value=self.root),
|
|
mock.patch('queue_log.application_data_dir', return_value=self.root),
|
|
mock.patch('socket.create_connection', side_effect=AssertionError('network forbidden')),
|
|
mock.patch('ai_config.AI_REVIEW_RULES', []),
|
|
):
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
self.bot = self.make_bot()
|
|
self.key = session_key(ACCOUNT, CONV)
|
|
|
|
def make_bot(self):
|
|
bot = ProtocolBot(db=self.db, sender=self.sender, identity={'accountId': ACCOUNT, 'pid': 1},
|
|
path=self.root / 'protocol.json', generate=self.generate, log=lambda _: None)
|
|
bot.message_batch_window_seconds = 0
|
|
bot.send_delay_seconds = 0
|
|
return bot
|
|
|
|
def enqueue(self, item=None):
|
|
self.db.add(item or message())
|
|
self.bot.poll_once()
|
|
return self.bot._pending.get(self.key)
|
|
|
|
def saved(self):
|
|
return json.loads(self.bot.path.read_text(encoding='utf-8'))['pending'][self.key]
|
|
|
|
def test_review_draft_reason_persists_then_explicit_approval_sends_once(self):
|
|
self.bot.send_mode = 'review'
|
|
state = self.enqueue()
|
|
self.assertTrue(state['awaiting_review'])
|
|
self.assertIn('人工审核', state['review_reason'])
|
|
self.assertEqual(state['staged_reply_text'], '合成回复草稿')
|
|
self.assertEqual(self.saved()['review_reason'], state['review_reason'])
|
|
self.assertFalse(self.sender.calls)
|
|
self.assertEqual(self.bot.approve_pending_replies([self.key])['approved'], [self.key])
|
|
self.assertEqual(self.saved()['stage'], 'ready_to_send')
|
|
self.assertFalse(self.saved()['awaiting_review'])
|
|
self.bot.poll_once()
|
|
self.bot.poll_once()
|
|
self.assertEqual(len(self.sender.calls), 1)
|
|
|
|
def test_high_risk_reason_available_in_automatic_mode(self):
|
|
self.bot.generate = lambda _: (self.bot._pending[self.key].update(judge_risk='high') or '高风险草稿')
|
|
state = self.enqueue()
|
|
self.assertTrue(state['awaiting_review'])
|
|
self.assertEqual(state['review_reason'], '模型裁判评估为高风险')
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_media_batch_is_visible_manual_work_without_approvable_draft(self):
|
|
self.db.add({**message(2, text='合成图片'), 'content_type': 3})
|
|
state = self.enqueue(message(3, text='请看上面的图'))
|
|
self.assertEqual(state['stage'], 'error')
|
|
self.assertTrue(state['manual_required'])
|
|
self.assertEqual(state['manual_reason'], MANUAL_MEDIA_REASON)
|
|
self.assertIn('合成图片', state['last_lines'])
|
|
self.assertIn('请看上面的图', state['chat_text'])
|
|
self.assertFalse(state['awaiting_review'])
|
|
self.assertEqual(self.bot.approve_pending_replies([self.key])['not_pending'], [self.key])
|
|
self.assertFalse(self.sender.calls)
|
|
self.generate.assert_not_called()
|
|
|
|
def test_overlong_batch_cannot_be_approved_as_a_draft(self):
|
|
self.db.messages = [message(i) for i in range(1, 102)]
|
|
self.db.events = [self.db.messages[-1]]
|
|
self.bot.poll_once()
|
|
state = self.saved()
|
|
self.assertTrue(state['manual_required'])
|
|
self.assertEqual(state['manual_reason'], MANUAL_BATCH_REASON)
|
|
self.assertEqual(self.bot.approve_pending_replies([self.key])['not_pending'], [self.key])
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_retry_clears_old_metadata_then_rechecks_media_without_sending(self):
|
|
self.enqueue({**message(2), 'content_type': 3})
|
|
self.bot._pending[self.key].update(staged_reply_text='旧草稿', review_reason='旧审核原因')
|
|
self.assertEqual(self.bot.retry_pending_replies([self.key])['retried'], [self.key])
|
|
queued = self.saved()
|
|
for field in ('manual_required', 'manual_reason', 'review_reason', 'staged_reply_text', 'awaiting_review'):
|
|
self.assertFalse(queued[field], field)
|
|
self.bot.poll_once()
|
|
self.assertTrue(self.saved()['manual_required'])
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_next_valid_batch_discards_old_manual_requirement(self):
|
|
self.enqueue({**message(2), 'content_type': 3})
|
|
self.db.add(message(3, self=True, text='人工已处理图片'))
|
|
self.bot.send_mode = 'review'
|
|
state = self.enqueue(message(4, text='新的文本问题'))
|
|
self.assertFalse(state['manual_required'])
|
|
self.assertFalse(state['manual_reason'])
|
|
self.assertTrue(state['awaiting_review'])
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_old_saved_review_and_manual_tasks_restore_metadata_only(self):
|
|
self.bot.send_mode = 'review'
|
|
state = self.enqueue()
|
|
state.pop('review_reason')
|
|
self.bot._save()
|
|
restored = self.make_bot()
|
|
self.assertIn('人工审核', restored._pending[self.key]['review_reason'])
|
|
self.assertFalse(restored._pending[self.key].get('approved'))
|
|
state = restored._pending[self.key]
|
|
state.update(stage='error', last_error=MANUAL_MEDIA_REASON, awaiting_review=False, reply_text='')
|
|
state.pop('manual_required', None)
|
|
state.pop('manual_reason', None)
|
|
restored._save()
|
|
restored = self.make_bot()
|
|
self.assertEqual(restored._pending[self.key]['manual_reason'], MANUAL_MEDIA_REASON)
|
|
self.assertTrue(restored._pending[self.key]['manual_required'])
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_unknown_receipt_cannot_be_approved_or_retried(self):
|
|
self.sender.error = DeliveryUnknown('合成回执超时')
|
|
self.enqueue()
|
|
self.assertTrue(self.bot.needs_attention)
|
|
self.assertEqual(self.bot.approve_pending_replies([self.key])['protected'], [self.key])
|
|
self.assertEqual(self.bot.retry_pending_replies([self.key])['protected'], [self.key])
|
|
self.bot.poll_once()
|
|
self.assertEqual(len(self.sender.calls), 1)
|
|
|
|
def test_foreign_account_task_cannot_be_approved_deleted_or_retried(self):
|
|
self.bot.send_mode = 'review'
|
|
state = self.enqueue()
|
|
state['account'] = 'different-account'
|
|
self.bot._save()
|
|
for method in (self.bot.approve_pending_replies, self.bot.cancel_pending_replies, self.bot.retry_pending_replies):
|
|
self.assertEqual(method([self.key])['protected'], [self.key])
|
|
self.assertTrue(self.saved()['awaiting_review'])
|
|
self.assertFalse(self.saved().get('approved'))
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|