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

173 lines
9.5 KiB
Python

"""Manual handoff preserves uncertain delivery evidence without real sends."""
import json
import unittest
from unittest import mock
import test_visual_review_visibility as fixtures
class VisualHandoffTest(unittest.TestCase):
setUp = fixtures.VisualReviewVisibilityTest.setUp
def uncertain(self):
self.state.update(stage='uncertain', send_state='uncertain',
send_dispatched_at=123.0, generation_surface_signature=b'surface',
send_baseline_blocks=[['客户甲 9/16 10:00:00', '旧问题']],
last_lines=['客户甲 9/16 10:00:00', '旧问题'],
customer_speaker='客户甲')
return self.state
def handoff(self):
self.uncertain()
result = self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)
self.assertEqual(result['handed_off'], [self.key])
return result
def test_uncertain_requires_confirmation_and_keeps_evidence(self):
self.uncertain()
for confirm in (False, None, 'true', 1):
result = self.bot.handoff_pending_replies([self.key], confirm_uncertain=confirm)
self.assertEqual(result['requires_confirmation'], [self.key])
self.assertEqual(result['protected'], [self.key])
self.assertFalse(result['deleted'])
self.assertIs(self.bot._pending_reply_sessions[self.key], self.state)
def test_every_sending_stage_is_protected_even_when_confirmed(self):
for stage, send_state in [('sending', ''), ('sending', 'uncertain'), ('queued', 'sending')]:
self.state.update(stage=stage, send_state=send_state)
result = self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)
self.assertEqual(result['protected'], [self.key])
self.assertFalse(result['deleted'])
def test_receipt_unknown_states_always_require_confirmation(self):
for stage, send_state in [('receipt_check', ''), ('unknown', ''), ('queued', 'unknown'),
('queued', 'sent_uncommitted')]:
self.state.update(stage=stage, send_state=send_state)
result = self.bot.handoff_pending_replies([self.key])
self.assertEqual(result['requires_confirmation'], [self.key])
def test_confirmed_handoff_preserves_serialized_evidence_not_delivery(self):
self.handoff()
saved = json.loads(self.path.read_text(encoding='utf-8'))
self.assertEqual(saved['pending'], {})
entry = saved['manual_handoffs'][self.key]
self.assertEqual(entry['state']['send_state'], 'uncertain')
self.assertEqual(entry['state']['generation_surface_signature'], b'surface'.hex())
self.assertTrue(entry['receipt_checked_by_user'])
self.assertIs(entry['delivery_confirmed'], False)
self.assertEqual(entry['state']['staged_reply_text'], '合成待审草稿')
self.assertTrue(self.bot._reply_task_cancelled(self.fp))
def test_restart_and_unread_scan_cannot_resend_original(self):
self.handoff()
self.bot._cancelled_reply_sessions = set()
self.bot._pending_reply_sessions = self.bot._load_pending_replies()
self.assertEqual(self.bot._pending_reply_sessions, {})
self.assertTrue(self.bot._reply_task_cancelled(self.fp))
self.assertFalse(self.bot._begin_reply_pending_from_unread(self.fp, confirmed_unread=True))
self.assertEqual(self.bot._pending_reply_sessions, {})
self.assertTrue(self.bot._persist_pending_replies())
self.assertIn(self.key, json.loads(self.path.read_text(encoding='utf-8'))['manual_handoffs'])
def test_failed_handoff_save_rolls_back_pending_tombstone_and_evidence(self):
self.uncertain()
self.bot._cancelled_reply_sessions = {'other'}
with mock.patch.object(self.bot, '_persist_pending_replies_unlocked', return_value=False):
result = self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)
self.assertIn('error', result)
self.assertFalse(result['deleted'])
self.assertIs(self.bot._pending_reply_sessions[self.key], self.state)
self.assertEqual(self.bot._cancelled_reply_sessions, {'other'})
self.assertEqual(self.bot._manual_handoffs, {})
def test_generating_work_is_cancelled_without_approving_or_sending(self):
self.state.update(stage='generating', awaiting_review=False)
self.bot._pending_exchanges = {self.key: {'reply': '尚未生成完成'}}
result = self.bot.handoff_pending_replies([self.key])
self.assertEqual(result['handed_off'], [self.key])
self.assertTrue(self.bot._reply_task_cancelled(self.fp))
self.assertNotIn(self.key, self.bot._pending_exchanges)
self.assertFalse(self.bot._mark_reply_pending(self.fp))
self.assertFalse(self.state.get('review_approved'))
def test_old_transcript_or_staff_reply_does_not_unlock_handoff(self):
self.handoff()
self.bot._known_agent_speakers = {'客服甲'}
for text in ('客户甲 9/16 10:00:00\n旧问题',
'客户甲 9/16 10:00:00\n旧问题\n客服甲 9/16 10:01:00\n人工回答',
'客户甲 9/16 10:02:00\n无旧基线的新片段'):
self.assertFalse(self.bot._resume_handoff_for_new_message(self.fp, text))
self.assertTrue(self.bot._reply_task_cancelled(self.fp))
def test_proven_new_customer_block_resumes_but_retains_old_evidence(self):
self.handoff()
text = '客户甲 9/16 10:00:00\n旧问题\n客户甲 9/16 10:02:00\n追加的新问题'
self.assertTrue(self.bot._resume_handoff_for_new_message(self.fp, text))
self.assertFalse(self.bot._reply_task_cancelled(self.fp))
entry = json.loads(self.path.read_text(encoding='utf-8'))['manual_handoffs'][self.key]
self.assertGreater(entry['resumed_at'], 0)
self.assertEqual(entry['state']['send_state'], 'uncertain')
self.assertFalse(entry['delivery_confirmed'])
def test_resume_does_not_take_effect_if_disk_save_fails(self):
self.handoff()
text = '客户甲 9/16 10:00:00\n旧问题\n客户甲 9/16 10:02:00\n追加的新问题'
with mock.patch.object(self.bot, '_persist_pending_replies_unlocked', return_value=False):
self.assertFalse(self.bot._resume_handoff_for_new_message(self.fp, text))
self.assertTrue(self.bot._reply_task_cancelled(self.fp))
def test_db_message_must_be_newer_and_same_conversation(self):
self.state['database_event'] = {'account': 'A', 'conv_id': 'C', 'message_id': 'old', 'send_time': 100}
self.handoff()
old = dict(self.state['database_event'])
for event in (old, {**old, 'message_id': 'new', 'send_time': 99},
{**old, 'message_id': 'new', 'send_time': 101, 'conv_id': 'other'}):
self.assertFalse(self.bot._resume_handoff_for_new_message(self.fp, database_event=event))
self.assertTrue(self.bot._resume_handoff_for_new_message(
self.fp, database_event={**old, 'message_id': 'new', 'send_time': 101}))
self.assertFalse(self.bot._reply_task_cancelled(self.fp))
def test_old_database_event_is_not_requeued_after_restart(self):
event = {'account': 'A', 'conv_id': 'C', 'message_id': 'old', 'send_time': 100}
self.state['database_event'] = event
self.handoff()
self.bot._cancelled_reply_sessions = set()
self.bot._pending_reply_sessions = self.bot._load_pending_replies()
okay, reason = self.bot.enqueue_detected(self.key, display_name='客户甲', database_event=event)
self.assertFalse(okay)
self.assertIn('转人工', reason)
self.assertEqual(self.bot._pending_reply_sessions, {})
def test_offline_flat_handoff_metadata_loads_without_becoming_a_task(self):
entry = {'state': dict(self.state), 'handed_off_at': 1, 'delivery_confirmed': False}
self.path.write_text(json.dumps({'manual_handoffs': {self.key: entry}}), encoding='utf-8')
self.assertEqual(self.bot._load_pending_replies(), {})
self.assertTrue(self.bot._reply_task_cancelled(self.fp))
def test_plain_cancel_cannot_bypass_receipt_or_sending_guard(self):
for stage, send_state in [('receipt_check', ''), ('uncertain', ''), ('unknown', ''),
('sending', ''), ('queued', 'unknown')]:
self.state.update(stage=stage, send_state=send_state)
result = self.bot.cancel_pending_replies([self.key])
self.assertEqual(result['protected'], [self.key])
self.assertFalse(result['deleted'])
def test_new_task_cannot_receive_an_old_model_result_after_handoff(self):
self.state.update(stage='generating', awaiting_review=False)
self.bot._consume_database_update = mock.Mock()
self.bot.report_progress = mock.Mock()
def old_model_returns(*args, **kwargs):
self.bot.handoff_pending_replies([self.key])
# A subsequent proven customer message is a different task object.
self.bot._manual_handoffs[self.key]['resumed_at'] = 1
self.bot._cancelled_reply_sessions.clear()
self.bot._pending_reply_sessions[self.key] = {'stage': 'queued', 'staged_user_text': '后续新问题'}
return '旧模型回复'
self.bot._generate_ai_reply_impl = mock.Mock(side_effect=old_model_returns)
self.assertIsNone(self.bot._generate_ai_reply(self.fp, '旧问题'))
self.assertNotIn('staged_reply_text', self.bot._pending_reply_sessions[self.key])
if __name__ == '__main__':
unittest.main()