"""Exercise transcription -> question/risk/model/send using only synthetic transports.""" import time import unittest from unittest import mock from protocol_engine import session_key, model_context_text import test_native_protocol_pipeline as helpers from test_native_protocol_pipeline import message, ACCOUNT, CONV class ProtocolVoiceTests(unittest.TestCase): setUp = helpers.EngineTests.setUp make_bot = helpers.EngineTests.make_bot enqueue = helpers.EngineTests.enqueue def voice(self, n=2, status='pending', text='语音内容合成测试', **kwargs): m = message(n, **kwargs) m.update(content_type=16, voice_status=status, content='[语音待转文字]') if status == 'ready': m.update(voice_transcribed=True, content='[语音转文字] ' + text, voice_binding=f"{m['account']}\0{m['conv_id']}\0{m['server_id']}") return m def test_transcribed_voice_reaches_model_and_selective_review_as_text(self): from protocol_engine import ProtocolBot self.bot._needs_review = lambda state: ProtocolBot._needs_review(self.bot, state) self.bot._assistant_pending = lambda state: False v = self.voice(status='ready') with mock.patch('review_policy.selective_review_reason', return_value='合成审核规则') as policy: self.enqueue(v) self.gen.assert_called_once() context = self.gen.call_args.args[0] self.assertIn('语音内容合成测试', model_context_text(context)) state = self.bot._pending[session_key(ACCOUNT, CONV)] self.assertIn('语音内容合成测试', policy.call_args.args[1]) self.assertTrue(state['awaiting_review']) self.assertFalse(state.get('manual_required')) self.assertFalse(self.sender.calls) def test_pending_voice_delays_only_its_conversation_then_resumes_on_cache_update(self): v = self.voice() self.enqueue(v) key = session_key(ACCOUNT, CONV) self.assertEqual(self.bot._pending[key]['stage'], 'voice_transcribing') self.gen.assert_not_called() self.db.add(message(4, conv='S:100_201')) self.bot.poll_once() self.assertEqual([c[2] for c in self.sender.calls], ['S:100_201']) v.update(self.voice(status='ready')) # Same server ID/row; no new-message event. self.bot._pending[key]['ready_at'] = 0 self.bot.poll_once() self.assertEqual([c[2] for c in self.sender.calls], ['S:100_201', CONV]) self.assertFalse(self.bot.stop_event.is_set()) def test_mixed_voice_and_text_waits_and_preserves_both(self): v = self.voice() self.db.add(v) self.enqueue(message(3, text='补充文字')) key = session_key(ACCOUNT, CONV) self.assertEqual(self.bot._pending[key]['stage'], 'voice_transcribing') v.update(self.voice(status='ready')) self.bot._pending[key]['ready_at'] = 0 self.bot.poll_once() context = self.gen.call_args.args[0] self.assertIn('补充文字', model_context_text(context)) self.assertIn('语音内容合成测试', model_context_text(context)) self.assertEqual(len(self.sender.calls), 1) def test_failed_transcription_enters_manual_and_never_calls_model(self): v = self.voice(status='error') v['voice_error_code'] = 'empty_audio' self.enqueue(v) state = self.bot._pending[session_key(ACCOUNT, CONV)] self.assertTrue(state['manual_required']) self.assertIn('empty_audio', state['manual_reason']) self.gen.assert_not_called() self.assertFalse(self.sender.calls) def test_missing_voice_times_out_without_global_stop_or_guess(self): self.enqueue(self.voice(status='missing')) state = self.bot._pending[session_key(ACCOUNT, CONV)] state.update(voice_wait_started_at=time.time()-181, ready_at=0) self.bot.poll_once() self.assertTrue(state['manual_required']) self.assertIn('超时', state['manual_reason']) self.assertFalse(self.bot.stop_event.is_set()) self.gen.assert_not_called() def test_human_reply_while_voice_pending_discards_old_question(self): self.enqueue(self.voice()) self.db.add(message(3, self=True, text='人工答复')) self.bot._pending[session_key(ACCOUNT, CONV)]['ready_at'] = 0 self.bot.poll_once() self.assertFalse(self.bot._pending) self.gen.assert_not_called() self.assertFalse(self.sender.calls) def test_new_media_after_voice_still_needs_manual(self): self.db.add(self.voice(status='ready')) image = message(3) image['content_type'] = 3 self.enqueue(image) self.assertTrue(self.bot._pending[session_key(ACCOUNT, CONV)]['manual_required']) self.gen.assert_not_called() def test_spoofed_or_cross_conversation_transcription_does_not_reach_model(self): v = self.voice(status='ready') v['voice_binding'] = '101\0M:200\0'+'2' self.enqueue(v) self.gen.assert_not_called() self.assertFalse(self.sender.calls) self.assertEqual(self.bot._pending[session_key(ACCOUNT, CONV)]['stage'], 'voice_transcribing') if __name__ == '__main__': unittest.main()