# -*- coding: utf-8 -*- """回复队列的执行记录:写得下、读得出、坏了也不许把机器人带崩。""" import json import os import tempfile import time from unittest import TestCase, SkipTest, mock from queue_log import QueueLog import wechat_bot from wechat_bot import WeChatBot def _log(directory: str) -> QueueLog: return QueueLog(os.path.join(directory, "queue_events.json")) class QueueLogStoreTest(TestCase): def test_a_recorded_step_comes_back_with_everything_it_was_given(self): with tempfile.TemporaryDirectory() as directory: log = _log(directory) log.append("abc123", "高瑞@微信", "已发送", "好的,这就帮您看") entry = log.recent()[0] self.assertEqual(entry["session_id"], "abc123") self.assertEqual(entry["name"], "高瑞@微信") self.assertEqual(entry["event"], "已发送") self.assertEqual(entry["detail"], "好的,这就帮您看") self.assertGreater(entry["ts"], 0) def test_the_newest_step_is_the_one_you_see_first(self): """排查总是从"刚才发生了什么"开始,最新的必须在最上面。""" with tempfile.TemporaryDirectory() as directory: log = _log(directory) for event in ("入队", "开始处理", "已发送"): log.append("abc123", "高瑞@微信", event) self.assertEqual( [item["event"] for item in log.recent()], ["已发送", "开始处理", "入队"], ) def test_a_long_running_bot_never_grows_the_file_without_bound(self): with tempfile.TemporaryDirectory() as directory: log = _log(directory) log.MAX_EVENTS = 5 for index in range(12): log.append("abc123", "高瑞@微信", "已发送", str(index)) kept = log.recent(50) self.assertEqual(len(kept), 5) # 留下的必须是最近那几条,不能是最早的 self.assertEqual([item["detail"] for item in kept], ["11", "10", "9", "8", "7"]) def test_asking_for_fewer_steps_gives_exactly_that_many(self): with tempfile.TemporaryDirectory() as directory: log = _log(directory) for index in range(6): log.append("abc123", "高瑞@微信", "已发送", str(index)) self.assertEqual(len(log.recent(2)), 2) def test_reading_an_empty_history_is_not_an_error(self): with tempfile.TemporaryDirectory() as directory: self.assertEqual(_log(directory).recent(), []) def test_a_corrupted_history_starts_over_instead_of_crashing(self): """记录只是排查线索,它坏掉不能反过来让界面和机器人一起崩。""" with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "queue_events.json") with open(path, "w", encoding="utf-8") as handle: handle.write("{这不是 JSON") log = QueueLog(path) self.assertEqual(log.recent(), []) self.assertTrue(log.append("abc123", "高瑞@微信", "入队")) self.assertEqual(len(log.recent()), 1) def test_junk_rows_are_dropped_rather_than_handed_to_the_table(self): with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "queue_events.json") with open(path, "w", encoding="utf-8") as handle: json.dump({"events": ["坏行", {"event": "入队"}, 7]}, handle) self.assertEqual([item["event"] for item in QueueLog(path).recent()], ["入队"]) def test_a_finished_write_leaves_no_temp_files_behind(self): """写入走临时文件加原子替换,界面才不会读到写了一半的 JSON。""" with tempfile.TemporaryDirectory() as directory: log = _log(directory) log.append("abc123", "高瑞@微信", "入队") leftovers = [name for name in os.listdir(directory) if name.endswith(".tmp")] self.assertEqual(leftovers, []) def test_a_failed_write_reports_failure_instead_of_raising(self): with tempfile.TemporaryDirectory() as directory: log = _log(directory) with mock.patch.object(log, "_write_unlocked", side_effect=OSError("盘满了")): self.assertFalse(log.append("abc123", "高瑞@微信", "入队")) def test_clearing_wipes_the_history_and_survives_a_reread(self): with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "queue_events.json") log = QueueLog(path) log.append("abc123", "高瑞@微信", "入队") self.assertTrue(log.clear()) self.assertEqual(QueueLog(path).recent(), []) def test_a_novel_length_reply_does_not_bloat_every_row(self): with tempfile.TemporaryDirectory() as directory: log = _log(directory) log.append("abc123", "高瑞@微信", "已发送", "啊" * 5000) self.assertLessEqual(len(log.recent()[0]["detail"]), 200) class BotQueueRecordingTest(TestCase): """机器人把队列里的每一步记下来,且记录出问题时不许影响回复。""" @staticmethod def _bot(directory: str) -> WeChatBot: bot = WeChatBot.__new__(WeChatBot) bot._pending_reply_sessions = {} bot._pending_reply_path = os.path.join(directory, "pending.json") bot._queue_log_instance = _log(directory) return bot def test_a_new_task_writes_one_enqueue_line(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) fp = b"\x01" * 40 with mock.patch.object(WeChatBot, "_live_render_ids_for", return_value=()), \ mock.patch.object(WeChatBot, "_session_label", return_value="高瑞@微信"), \ mock.patch.object(WeChatBot, "_persist_pending_replies", return_value=True): bot._mark_reply_pending(fp, confirmed_unread=True) entries = bot.queue_log.recent() self.assertEqual([item["event"] for item in entries], ["入队"]) self.assertEqual(entries[0]["name"], "高瑞@微信") def test_the_same_task_touched_again_does_not_queue_twice(self): """一轮里同一个会话会被反复标记,每次都记一条"入队"就没法看了。""" with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) fp = b"\x01" * 40 with mock.patch.object(WeChatBot, "_live_render_ids_for", return_value=()), \ mock.patch.object(WeChatBot, "_session_label", return_value="高瑞@微信"), \ mock.patch.object(WeChatBot, "_persist_pending_replies", return_value=True): bot._mark_reply_pending(fp, confirmed_unread=True) bot._mark_reply_pending(fp, batch_ready=True) bot._mark_reply_pending(fp) self.assertEqual(len(bot.queue_log.recent()), 1) def test_the_nickname_is_kept_on_the_task_for_later_steps(self): """后面几步都要报出人名,不能每一步再去 OCR 一遍。""" with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) fp = b"\x01" * 40 with mock.patch.object(WeChatBot, "_live_render_ids_for", return_value=()), \ mock.patch.object(WeChatBot, "_session_label", return_value="高瑞@微信"), \ mock.patch.object(WeChatBot, "_persist_pending_replies", return_value=True): bot._mark_reply_pending(fp) self.assertEqual( bot._pending_reply_sessions[fp.hex()]["display_name"], "高瑞@微信" ) def test_an_unreadable_nickname_is_never_stored_as_the_customer(self): """读不出昵称时 `_session_label` 返回「会话 xxxx」,那是占位不是人名。""" with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) fp = b"\x01" * 40 with mock.patch.object(WeChatBot, "_live_render_ids_for", return_value=()), \ mock.patch.object(WeChatBot, "_session_label", return_value="会话 0101"), \ mock.patch.object(WeChatBot, "_persist_pending_replies", return_value=True): bot._mark_reply_pending(fp) self.assertNotIn("display_name", bot._pending_reply_sessions[fp.hex()]) def test_a_broken_recorder_never_stops_a_reply(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) with mock.patch.object( bot._queue_log_instance, "append", side_effect=RuntimeError("炸了") ): bot._log_queue_event(b"\x01" * 40, "已发送", "在的") def test_a_recorded_step_carries_the_session_id_the_ui_shows(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) fp = WeChatBot._fp_from_name("高瑞@微信") bot._pending_reply_sessions[fp.hex()] = {"display_name": "高瑞@微信"} bot._log_queue_event(fp, "已发送", "在的") self.assertEqual( bot.queue_log.recent()[0]["session_id"], WeChatBot.session_id_of(fp) ) def test_a_recorded_step_is_also_pushed_to_the_floating_capsule(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) fp = WeChatBot._fp_from_name("高瑞@微信") bot._pending_reply_sessions[fp.hex()] = {"display_name": "高瑞@微信"} bot.progress_cb = mock.Mock() bot._log_queue_event(fp, "开始处理", "已打开会话,正在读取消息") bot.progress_cb.assert_called_once_with("正在读取消息 · 高瑞@微信") def test_an_atomic_operation_is_visible_in_capsule_and_execution_table(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) fp = WeChatBot._fp_from_name("高瑞@微信") bot._pending_reply_sessions[fp.hex()] = {"display_name": "高瑞@微信"} bot.progress_cb = mock.Mock() bot.report_operation( "会话视觉定位", 2, 4, "检测聊天区、右侧工具抽屉和输入框", fp, ) bot.progress_cb.assert_called_once_with( "会话视觉定位 2/4 · 检测聊天区、右侧工具抽屉和输入框 · 高瑞@微信" ) entry = bot.queue_log.recent()[0] self.assertEqual(entry["event"], "会话视觉定位 2/4") self.assertEqual(entry["detail"], "检测聊天区、右侧工具抽屉和输入框") def test_visual_model_is_named_explicitly_in_capsule_progress(self): self.assertEqual( WeChatBot._queue_event_progress_text( "高瑞@微信", "调用模型", "视觉模型|客户发来图片" ), "正在调用视觉模型 · 高瑞@微信", ) def test_the_nickname_survives_a_restart(self): """重启后队列界面还要认得出这些任务是谁的。""" with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "pending.json") bot = self._bot(directory) bot._pending_reply_path = path fp = b"\x01" * 40 with mock.patch.object(WeChatBot, "_live_render_ids_for", return_value=()), \ mock.patch.object(WeChatBot, "_session_label", return_value="高瑞@微信"): bot._mark_reply_pending(fp, confirmed_unread=True) restored = WeChatBot.__new__(WeChatBot) restored._pending_reply_path = path self.assertEqual( restored._load_pending_replies()[fp.hex()]["display_name"], "高瑞@微信" ) class SendFailureReasonTest(TestCase): """失败要写清是哪一道闸拦的,光说"发送未成功"等于什么都没说。""" @staticmethod def _bot(directory: str, state: dict | None = None) -> WeChatBot: bot = WeChatBot.__new__(WeChatBot) bot._pending_reply_sessions = {(b"\x01" * 40).hex(): state} if state else {} bot._pending_reply_path = os.path.join(directory, "pending.json") bot._queue_log_instance = _log(directory) return bot def test_the_gate_that_blocked_the_send_is_named_in_the_record(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory, {"display_name": "高瑞@微信"}) fp = b"\x01" * 40 bot._deny_send("输入框里有人工草稿「稍等」,已保留不动") bot._log_send_outcome(fp, False) entry = bot.queue_log.recent()[0] self.assertEqual(entry["event"], "失败") self.assertIn("人工草稿", entry["detail"]) def test_a_pressed_enter_awaiting_proof_is_not_branded_a_failure(self): """回车已经按下去,只是还没认出那条消息——多半其实发出去了。""" with tempfile.TemporaryDirectory() as directory: state = {"display_name": "高瑞@微信", "send_state": "uncertain"} bot = self._bot(directory, state) fp = b"\x01" * 40 bot._deny_send("已按下发送但屏幕上还看不到这条,转入后台对账") bot._log_send_outcome(fp, False) self.assertEqual(bot.queue_log.recent()[0]["event"], "发送待核对") def test_a_successful_send_records_what_was_actually_said(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory, {"display_name": "高瑞@微信"}) bot._log_send_outcome(b"\x01" * 40, True, "在的,您慢慢说。") entry = bot.queue_log.recent()[0] self.assertEqual(entry["event"], "已发送") self.assertEqual(entry["detail"], "在的,您慢慢说。") def test_an_unnamed_reason_still_produces_a_usable_line(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory, {"display_name": "高瑞@微信"}) bot._last_send_failure_reason = "" bot._log_send_outcome(b"\x01" * 40, False) self.assertTrue(bot.queue_log.recent()[0]["detail"]) def test_a_new_send_forgets_the_previous_rounds_reason(self): """上一轮的原因留到这一轮,会把人引到早就修好的问题上。""" bot = WeChatBot.__new__(WeChatBot) bot._deny_send("上一轮:检测到人工草稿") bot._security_gate_visible = mock.Mock(return_value=True) bot.send_reply("在的", expected_fp=None) self.assertIn("安全验证", bot.last_send_failure_reason()) def test_every_gate_reason_is_human_readable_not_a_code(self): bot = WeChatBot.__new__(WeChatBot) bot._deny_send("发送限频未过,等待后仍不允许发送") reason = bot.last_send_failure_reason() self.assertNotIn("_", reason) self.assertGreater(len(reason), 4) class ModelCallRecordTest(TestCase): """记录里要看得出模型收到的是什么,否则"答非所问"根本无从查起。""" def test_a_plain_question_is_quoted_in_the_record(self): self.assertEqual( WeChatBot._model_call_summary("在不在", "在不在"), "客户说:在不在" ) def test_an_image_message_says_so_instead_of_looking_empty(self): summary = WeChatBot._model_call_summary("", "", {"image"}) self.assertIn("图片", summary) def test_an_image_with_a_caption_keeps_both(self): summary = WeChatBot._model_call_summary("这个多少钱", "", {"image"}) self.assertIn("图片", summary) self.assertIn("这个多少钱", summary) def test_several_media_kinds_are_all_named(self): summary = WeChatBot._model_call_summary("", "", {"image", "voice"}) self.assertIn("图片", summary) self.assertIn("语音", summary) def test_a_wall_of_text_is_trimmed_to_stay_readable(self): summary = WeChatBot._model_call_summary("啊" * 500, "") self.assertLessEqual(len(summary), 100) self.assertTrue(summary.endswith("…")) def test_a_screenshot_only_round_is_described_rather_than_left_blank(self): self.assertIn("截图", WeChatBot._model_call_summary("", "")) def test_line_breaks_never_break_the_table_row(self): summary = WeChatBot._model_call_summary("第一行\n第二行", "") self.assertNotIn("\n", summary) class GenerationGapTest(TestCase): """模型没交出回复时,队列里不能出现"入队之后再无下文"的断头任务。""" @staticmethod def _bot(directory: str) -> WeChatBot: bot = WeChatBot.__new__(WeChatBot) bot._pending_reply_sessions = {} bot._pending_reply_path = os.path.join(directory, "pending.json") bot._queue_log_instance = _log(directory) return bot def test_a_silent_model_still_leaves_a_line_in_the_record(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) bot._generation_logged = False bot._log_generation_gap(b"\x01" * 40) self.assertEqual(bot.queue_log.recent()[0]["event"], "跳过") def test_a_model_that_already_explained_itself_is_not_repeated(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) bot._log_queue_event(b"\x01" * 40, "跳过", "模型判定没有新消息") bot._log_generation_gap(b"\x01" * 40) self.assertEqual(len(bot.queue_log.recent()), 1) def test_a_recorded_reply_also_counts_as_the_model_speaking_up(self): with tempfile.TemporaryDirectory() as directory: bot = self._bot(directory) bot._log_queue_event(b"\x01" * 40, "生成回复", "在的") bot._log_generation_gap(b"\x01" * 40) self.assertEqual(len(bot.queue_log.recent()), 1) class QueuePageReadingTest(TestCase): """队列页面怎么把磁盘上那两个文件讲成人话。""" @classmethod def setUpClass(cls): try: from wechat_gui_qt import QueuePage except Exception as exc: # PySide6 缺失/无显示环境时跳过 raise SkipTest(f"PySide6 不可用: {exc}") cls.page = QueuePage def test_a_task_being_sent_reads_as_sending_not_as_queued(self): self.assertEqual(self.page._state_text({"send_state": "sending"}), "正在发送") def test_a_task_whose_receipt_is_unconfirmed_says_so(self): """这个状态正是"回了没回"说不清的时候,界面上必须看得见。""" self.assertEqual(self.page._state_text({"send_state": "uncertain"}), "发送待核对") def test_a_batched_task_reads_as_generating(self): self.assertEqual(self.page._state_text({"batch_ready": True}), "正在生成回复") def test_a_fresh_unread_task_reads_as_waiting(self): self.assertEqual(self.page._state_text({"confirmed_unread": True}), "等待处理") def test_a_bare_task_still_gets_a_readable_status(self): self.assertEqual(self.page._state_text({}), "排队中") def test_a_short_wait_is_shown_in_seconds(self): import time as _time self.assertTrue(self.page._waited_text(_time.time() - 12).endswith("秒")) def test_a_long_wait_is_shown_in_hours_so_a_stuck_task_stands_out(self): import time as _time self.assertIn("小时", self.page._waited_text(_time.time() - 7200)) def test_a_task_with_no_timestamp_never_shows_a_bogus_wait(self): self.assertEqual(self.page._waited_text(None), "--") class QueuePageTableTest(TestCase): """页面真渲染出来之后,表格里到底是什么。""" @classmethod def setUpClass(cls): try: from PySide6.QtWidgets import QApplication import wechat_gui_qt cls.app = QApplication.instance() or QApplication([]) cls.module = wechat_gui_qt except Exception as exc: # PySide6 缺失/无显示环境时跳过 raise SkipTest(f"PySide6 不可用: {exc}") def _page(self, directory: str, pending: dict, log: QueueLog): from pathlib import Path with open(os.path.join(directory, "pending_replies.json"), "w", encoding="utf-8") as handle: json.dump(pending, handle, ensure_ascii=False) with mock.patch.object(self.module, "SCRIPT_DIR", Path(directory)), \ mock.patch("queue_log.QueueLog", lambda *a, **k: log): page = self.module.QueuePage() page.refresh_data() return page @staticmethod def _column(table, column: int) -> list[str]: return [ table.item(row, column).text() if table.item(row, column) else "" for row in range(table.rowCount()) ] def test_the_queue_is_shown_oldest_first_so_nobody_is_starved(self): """界面上的顺序就是实际服务顺序,先来的必须排在前面。""" with tempfile.TemporaryDirectory() as directory: now = time.time() pending = { "aa" * 20: {"display_name": "后来的", "created_at": now - 5}, "bb" * 20: {"display_name": "先来的", "created_at": now - 500}, } page = self._page(directory, pending, _log(directory)) self.assertEqual(self._column(page.queue_table, 1), ["先来的", "后来的"]) self.assertEqual(page.waiting_metric.value.text(), "2") def test_an_empty_queue_says_so_instead_of_showing_a_blank_slab(self): with tempfile.TemporaryDirectory() as directory: page = self._page(directory, {}, _log(directory)) self.assertEqual(page.queue_table.rowCount(), 1) self.assertIn("没有排队", page.queue_table.item(0, 0).text()) self.assertEqual(page.waiting_metric.value.text(), "0") def test_a_queue_that_drains_replaces_its_rows_with_the_empty_notice(self): """刷新是原地重画的,上一轮的行不清干净就会留在屏幕上骗人。""" with tempfile.TemporaryDirectory() as directory: from pathlib import Path log = _log(directory) path = os.path.join(directory, "pending_replies.json") page = self._page( directory, {"aa" * 20: {"display_name": "高瑞@微信", "created_at": time.time()}}, log, ) self.assertEqual(page.queue_table.rowCount(), 1) with open(path, "w", encoding="utf-8") as handle: json.dump({}, handle) with mock.patch.object(self.module, "SCRIPT_DIR", Path(directory)), \ mock.patch("queue_log.QueueLog", lambda *a, **k: log): page.refresh_data() self.assertIn("没有排队", page.queue_table.item(0, 0).text()) def test_a_task_whose_nickname_never_read_still_takes_a_visible_slot(self): """认不出是谁也得让人看见它在排队,否则就成了看不见的积压。""" with tempfile.TemporaryDirectory() as directory: pending = {"cc" * 20: {"created_at": time.time(), "send_state": "uncertain"}} page = self._page(directory, pending, _log(directory)) self.assertEqual(page.queue_table.rowCount(), 1) self.assertIn("未识别", page.queue_table.item(0, 1).text()) def test_todays_sends_and_failures_are_counted_from_the_history(self): with tempfile.TemporaryDirectory() as directory: log = _log(directory) for event in ("已发送", "已发送", "失败", "跳过", "入队"): log.append("abc", "高瑞@微信", event) page = self._page(directory, {}, log) self.assertEqual(page.sent_metric.value.text(), "2") self.assertEqual(page.failed_metric.value.text(), "1") def test_yesterdays_sends_do_not_inflate_todays_count(self): with tempfile.TemporaryDirectory() as directory: path = os.path.join(directory, "queue_events.json") stale = time.time() - 3 * 24 * 3600 with open(path, "w", encoding="utf-8") as handle: json.dump( {"events": [{"ts": stale, "name": "高瑞@微信", "event": "已发送"}]}, handle, ) page = self._page(directory, {}, QueueLog(path)) self.assertEqual(page.history_table.rowCount(), 1) self.assertEqual(page.sent_metric.value.text(), "0") def test_a_task_with_a_junk_timestamp_still_appears_in_the_queue(self): """一条坏数据不能让整页读不出来——那正是最需要看它的时候。""" with tempfile.TemporaryDirectory() as directory: pending = { "aa" * 20: {"display_name": "正常的", "created_at": time.time()}, "bb" * 20: {"display_name": "坏时间", "created_at": "昨天"}, } page = self._page(directory, pending, _log(directory)) self.assertEqual(self._column(page.queue_table, 1), ["正常的", "坏时间"]) self.assertEqual(page.queue_table.item(1, 3).text(), "--") def test_a_stopped_listener_explains_why_the_wait_keeps_growing(self): """停着的时候等待时长照涨,不说明白会被当成机器人卡死了。""" with tempfile.TemporaryDirectory() as directory: pending = {"aa" * 20: {"display_name": "高瑞@微信", "created_at": time.time()}} page = self._page(directory, pending, _log(directory)) page.set_running(False) self.assertTrue(page.queue_notice.isVisibleTo(page)) self.assertIn("监听已停止", page.queue_notice.text()) def test_a_running_listener_does_not_nag(self): with tempfile.TemporaryDirectory() as directory: pending = {"aa" * 20: {"display_name": "高瑞@微信", "created_at": time.time()}} page = self._page(directory, pending, _log(directory)) page.set_running(True) self.assertFalse(page.queue_notice.isVisibleTo(page)) def test_an_empty_queue_never_shows_the_stopped_warning(self): with tempfile.TemporaryDirectory() as directory: page = self._page(directory, {}, _log(directory)) page.set_running(False) self.assertFalse(page.queue_notice.isVisibleTo(page)) def test_a_task_awaiting_proof_is_coloured_apart_from_a_real_failure(self): with tempfile.TemporaryDirectory() as directory: log = _log(directory) log.append("abc", "高瑞@微信", "发送待核对", "已按下发送,正在核对") log.append("abc", "高瑞@微信", "失败", "输入框里有人工草稿") page = self._page(directory, {}, log) colours = { page.history_table.item(row, 2).text(): page.history_table.item(row, 2).foreground().color().name() for row in range(page.history_table.rowCount()) } self.assertNotEqual(colours["发送待核对"], colours["失败"]) def test_awaiting_proof_is_not_counted_as_a_failure_today(self): """把"多半已经发出去了"记成失败,会让人以为出了大问题。""" with tempfile.TemporaryDirectory() as directory: log = _log(directory) log.append("abc", "高瑞@微信", "发送待核对", "已按下发送,正在核对") page = self._page(directory, {}, log) self.assertEqual(page.failed_metric.value.text(), "0") def test_an_empty_history_says_so_too(self): with tempfile.TemporaryDirectory() as directory: page = self._page(directory, {}, _log(directory)) self.assertEqual(page.history_table.rowCount(), 1) self.assertIn("还没有", page.history_table.item(0, 0).text()) def test_a_missing_pending_file_is_not_reported_as_an_error(self): """第一次启动时这个文件根本不存在,不该在日志里吓人一跳。""" from pathlib import Path with tempfile.TemporaryDirectory() as directory: log = _log(directory) complaints = [] with mock.patch.object(self.module, "SCRIPT_DIR", Path(directory)), \ mock.patch("queue_log.QueueLog", lambda *a, **k: log): page = self.module.QueuePage() page.logMessage.connect(lambda text, tag: complaints.append(text)) page.refresh_data() self.assertEqual(complaints, [])