144 lines
7.2 KiB
Python
144 lines
7.2 KiB
Python
"""Explicit takeover never confirms/retries a possibly sent response."""
|
|
import json
|
|
from pathlib import Path
|
|
import queue
|
|
import tempfile
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from gui_runtime import BotThread, handoff_pending_reply_file
|
|
|
|
|
|
class RuntimeHandoffTests(unittest.TestCase):
|
|
def test_handoff_during_startup_keeps_takeover_semantics(self):
|
|
thread = BotThread(queue.Queue(), '合成回复', .2)
|
|
result = thread.handoff_pending_tasks(['pending-1'])
|
|
self.assertEqual(result['scheduled'], ['pending-1'])
|
|
bot = SimpleNamespace(handoff_pending_replies=mock.Mock(return_value={'handed_off': ['pending-1']}),
|
|
cancel_pending_replies=mock.Mock())
|
|
thread._drain_queued_handoffs(bot)
|
|
bot.handoff_pending_replies.assert_called_once_with({'pending-1'}, confirm_uncertain=False)
|
|
bot.cancel_pending_replies.assert_not_called()
|
|
thread._drain_queued_handoffs(bot)
|
|
bot.handoff_pending_replies.assert_called_once()
|
|
|
|
def test_live_protocol_takeover_uses_distinct_engine_action(self):
|
|
thread = BotThread(queue.Queue(), '合成回复', .2)
|
|
bot = SimpleNamespace(handoff_pending_replies=mock.Mock(return_value={'handed_off': ['pending-1']}),
|
|
cancel_pending_replies=mock.Mock())
|
|
thread.bot = bot
|
|
self.assertEqual(thread.handoff_pending_tasks(['pending-1'])['handed_off'], ['pending-1'])
|
|
bot.cancel_pending_replies.assert_not_called()
|
|
|
|
def test_visual_engine_falls_back_to_guarded_cancellation(self):
|
|
thread = BotThread(queue.Queue(), '合成回复', .2)
|
|
thread.bot = SimpleNamespace(cancel_pending_replies=mock.Mock(return_value={'deleted': ['pending-1'], 'protected': ['sending']}))
|
|
result = thread.handoff_pending_tasks(['pending-1', 'sending'])
|
|
self.assertEqual(result['handed_off'], ['pending-1'])
|
|
self.assertEqual(result['protected'], ['sending'])
|
|
|
|
def test_offline_takeover_preserves_dedup_and_does_not_claim_delivery(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
path = root / 'pending_replies.protocol.json'
|
|
raw = {'transport': 'protocol', 'pending': {
|
|
'unknown': {'send_state': 'uncertain', 'dedup_key': 'event-1', 'account': '100'},
|
|
'sending': {'send_state': 'sending', 'dedup_key': 'event-2', 'account': '100'},
|
|
}, 'seen': {'previous': 'event-0'}, 'contactBlocked': {}}
|
|
path.write_text(json.dumps(raw), encoding='utf-8')
|
|
journal = root / 'protocol_sends.sqlite3'
|
|
journal.write_bytes(b'synthetic unchanged journal')
|
|
with mock.patch('queue_log.application_data_dir', return_value=root):
|
|
result = handoff_pending_reply_file(['unknown', 'sending', 'absent'], str(path), confirm_uncertain=True)
|
|
self.assertEqual(result['handed_off'], ['unknown'])
|
|
self.assertEqual(result['protected'], ['sending'])
|
|
self.assertEqual(result['missing'], ['absent'])
|
|
saved = json.loads(path.read_text(encoding='utf-8'))
|
|
self.assertEqual(saved['seen'], {'previous': 'event-0', 'unknown': 'event-1'})
|
|
self.assertEqual(saved['pending'], {'sending': raw['pending']['sending']})
|
|
self.assertEqual(journal.read_bytes(), b'synthetic unchanged journal')
|
|
|
|
|
|
try:
|
|
from protocol_engine import ProtocolBot, session_key
|
|
from test_native_protocol_pipeline import Database, Sender, message, ACCOUNT, CONV
|
|
except ImportError:
|
|
ProtocolBot = None
|
|
|
|
|
|
@unittest.skipIf(ProtocolBot is None, 'visual-only distribution has no protocol engine')
|
|
class ProtocolHandoffTests(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.sender.lookup = mock.Mock(return_value=None)
|
|
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')),
|
|
):
|
|
patcher.start()
|
|
self.addCleanup(patcher.stop)
|
|
self.bot = self.make_bot()
|
|
self.key = session_key(ACCOUNT, CONV)
|
|
self.bot._pending[self.key] = dict(account=ACCOUNT, conv_id=CONV, send_state='uncertain',
|
|
stage='receipt_check', dedup_key=message()['dedup_key'], reply_text='可能已发送的草稿')
|
|
self.bot.needs_attention = True
|
|
self.bot._save()
|
|
|
|
def make_bot(self):
|
|
return ProtocolBot(db=self.db, sender=self.sender, identity={'accountId': ACCOUNT, 'pid': 1},
|
|
path=self.root/'queue.json', generate=self.generate, log=lambda _: None)
|
|
|
|
def test_explicit_takeover_releases_pause_without_send_or_receipt_change(self):
|
|
self.sender.lookup.reset_mock()
|
|
result = self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)
|
|
self.assertEqual(result['handed_off'], [self.key])
|
|
self.assertFalse(self.bot.needs_attention)
|
|
self.assertNotIn(self.key, self.bot._pending)
|
|
self.assertEqual(self.bot._seen[self.key], message()['dedup_key'])
|
|
self.sender.lookup.assert_not_called()
|
|
self.assertFalse(self.sender.calls)
|
|
self.generate.assert_not_called()
|
|
self.db.events = [message()]
|
|
self.bot.poll_once()
|
|
self.assertFalse(self.bot._pending)
|
|
self.assertFalse(self.sender.calls)
|
|
restored = self.make_bot()
|
|
self.db.events = [message()]
|
|
restored.poll_once()
|
|
self.assertFalse(restored.needs_attention)
|
|
self.assertFalse(restored._pending)
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_other_uncertain_tasks_keep_pause(self):
|
|
other = session_key(ACCOUNT, 'S:100_201')
|
|
self.bot._pending[other] = dict(self.bot._pending[self.key], conv_id='S:100_201')
|
|
self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)
|
|
self.assertTrue(self.bot.needs_attention)
|
|
self.assertIn(other, self.bot._pending)
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_active_send_and_other_account_stay_protected(self):
|
|
self.bot._pending[self.key]['send_state'] = 'sending'
|
|
self.assertEqual(self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)['protected'], [self.key])
|
|
self.bot._pending[self.key].update(send_state='uncertain', account='other-account')
|
|
self.assertEqual(self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)['protected'], [self.key])
|
|
self.assertIn(self.key, self.bot._pending)
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
def test_failed_send_can_be_taken_over_without_retry(self):
|
|
self.bot._pending[self.key].update(send_state='failed', stage='failed')
|
|
self.assertEqual(self.bot.handoff_pending_replies([self.key], confirm_uncertain=True)['handed_off'], [self.key])
|
|
self.assertFalse(self.bot._pending)
|
|
self.assertFalse(self.sender.calls)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|