"""Native Qt queue -> web snapshot -> explicit operator action regressions.""" import json import tempfile import time from pathlib import Path from types import SimpleNamespace, MethodType from unittest import TestCase, mock from PySide6.QtWidgets import QApplication, QMessageBox import wechat_gui_qt as qt from review_state import task_review_state class ReviewStateTest(TestCase): def test_protocol_and_visual_reviews_have_the_same_action_contract(self): for stage in ('awaiting_review','manual_review'): with self.subTest(stage=stage): result=task_review_state({'stage':stage,'reply_text':'合成草稿','stage_detail':'需要核对测试规则'}) self.assertTrue(result['awaitingReview']);self.assertTrue(result['canApprove']) self.assertFalse(result['needsHuman']);self.assertIn('测试规则',result['reviewReason']) def test_media_without_draft_cannot_be_approved(self): result=task_review_state({'stage':'error','manual_required':True,'manual_reason':'图片需要人工处理'}) self.assertTrue(result['needsHuman']);self.assertFalse(result['canApprove']);self.assertFalse(result['canRetry']) def test_uncertain_delivery_cannot_be_approved_or_retried_even_with_stale_review_flag(self): result=task_review_state({'stage':'receipt_check','send_state':'uncertain','reply_text':'可能已经发出','awaiting_review':True}) self.assertTrue(result['deliveryUncertain']);self.assertTrue(result['needsHuman']) self.assertFalse(result['canApprove']);self.assertFalse(result['canRetry']) def test_generation_failure_is_retryable_but_not_sendable(self): result=task_review_state({'stage':'error','stage_error':'模型调用失败'}) self.assertTrue(result['canRetry']);self.assertFalse(result['canApprove']) class DesktopReviewTest(TestCase): @classmethod def setUpClass(cls): cls.app=QApplication.instance() or QApplication([]) def setUp(self): self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup) self.path=Path(self.temp.name)/'pending.json' self.path.write_text('{}',encoding='utf-8') if hasattr(qt,'queue_path_for_transport'): patch=mock.patch.object(qt,'queue_path_for_transport',return_value=self.path) else: patch=mock.patch.object(qt,'SCRIPT_DIR',Path(self.temp.name)) self.path=Path(self.temp.name)/'pending_replies.json';self.path.write_text('{}',encoding='utf-8') patch.start();self.addCleanup(patch.stop) self.page=qt.QueuePage();self.addCleanup(self.page.deleteLater) self.host=SimpleNamespace(queue_page=self.page,runtime_settings={'mask_sensitive':False,'reply_transport':'protocol'}, _console_selected_key='',_running=False,_thread=None,log_page=SimpleNamespace(), _console_wecom_info=lambda:{'connected':False},append_log=mock.Mock(),_schedule_console_push=mock.Mock()) for name in ('_console_people','_console_task','_console_metrics','_console_stage_durations'): setattr(self.host,name,MethodType(getattr(qt.MainWindow,name),self.host)) for name in ('_console_clock_label','_console_seconds_label','_console_wait_seconds','_console_queue_wait_label'): setattr(self.host,name,getattr(qt.MainWindow,name)) def seed(self): common={'created_at':time.time(),'display_name':'合成客户','confirmed_unread':True,'staged_user_text':'合成问题'} states={'ordinary':{**common,'stage':'queued'}, 'review':{**common,'stage':'awaiting_review','reply_text':'待审核合成回复','review_reason':'命中测试规则','detected_by':'protocol_db'}, 'human':{**common,'stage':'error','manual_required':True,'manual_reason':'需人工看图'}, 'uncertain':{**common,'stage':'receipt_check','send_state':'uncertain','reply_text':'待核对合成回复'}} self.path.write_text(json.dumps({'pending':states}),encoding='utf-8') self.page.refresh_data() def test_review_filter_surfaces_protocol_draft_and_reason(self): self.seed();self.page._set_task_filter('待审核') people=self.host._console_people() self.assertEqual([row['key'] for row in people],['review']) self.assertEqual(people[0]['status'],'待审核') task=self.host._console_task(people) self.assertEqual(task['answer'],'待审核合成回复');self.assertIn('测试规则',task['reviewReason']) self.assertTrue(task['canApprove']);self.assertTrue(task['isProtocol']) def test_human_filter_includes_unknown_receipt_and_no_empty_send(self): self.seed();self.page._set_task_filter('需人工');people=self.host._console_people() self.assertEqual({row['key'] for row in people},{'human','uncertain'}) self.host._console_selected_key='uncertain';task=self.host._console_task(people) self.assertTrue(task['deliveryUncertain']);self.assertFalse(task['canApprove']);self.assertFalse(task['canRetry']) self.assertIn('不会自动重发',task['reviewReason']) def test_review_counts_remain_visible_under_an_unrelated_filter(self): self.seed();self.page._set_task_filter('等待中') metrics=self.host._console_metrics(self.host._console_people(),[]) self.assertEqual(metrics['review'],'1');self.assertEqual(metrics['human'],'2') def test_explicit_approval_reaches_thread_with_exact_key_and_refreshes(self): self.seed();self.host._thread=SimpleNamespace(is_alive=lambda:True,approve_pending_tasks=mock.Mock(return_value={'approved':['review']})) with mock.patch.object(self.page,'refresh_data',wraps=self.page.refresh_data) as refresh: qt.MainWindow._approve_queue_tasks(self.host,['review']) self.host._thread.approve_pending_tasks.assert_called_once_with(['review']);refresh.assert_called_once() def test_unknown_receipt_handoff_requires_confirmation_and_never_calls_approve(self): self.seed();thread=SimpleNamespace(is_alive=lambda:True,handoff_pending_tasks=mock.Mock(return_value={'handed_off':['uncertain']}),approve_pending_tasks=mock.Mock()) self.host._thread=thread with mock.patch.object(QMessageBox,'question',return_value=QMessageBox.StandardButton.No): qt.MainWindow._handoff_queue_tasks(self.host,['uncertain']) thread.handoff_pending_tasks.assert_not_called() with mock.patch.object(QMessageBox,'question',return_value=QMessageBox.StandardButton.Yes): qt.MainWindow._handoff_queue_tasks(self.host,['uncertain']) thread.handoff_pending_tasks.assert_called_once_with(['uncertain']);thread.approve_pending_tasks.assert_not_called() def test_stopped_handoff_uses_offline_resolution_and_preserves_seen(self): self.seed();raw=json.loads(self.path.read_text(encoding='utf-8'));raw['seen']={'synthetic-dedup':123};self.path.write_text(json.dumps(raw),encoding='utf-8') with mock.patch.object(QMessageBox,'question',return_value=QMessageBox.StandardButton.Yes): qt.MainWindow._handoff_queue_tasks(self.host,['uncertain']) result=json.loads(self.path.read_text(encoding='utf-8')) self.assertNotIn('uncertain',result['pending']);self.assertEqual(result['seen'],{'synthetic-dedup':123}) self.assertIn('review',result['pending']) def test_async_protocol_check_retains_explicit_approval_request(self): if not hasattr(qt,'protocol_ready'): self.skipTest('This directory has no protocol transport') self.host.start_monitoring=mock.Mock(side_effect=lambda:setattr(self.host,'_protocol_start_pending',True)) with mock.patch.object(QMessageBox,'warning') as warning: qt.MainWindow._approve_queue_tasks(self.host,['review']) self.assertEqual(self.host._deferred_review_approvals,['review']);warning.assert_not_called()