from __future__ import annotations import os import sys import unittest from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, patch BACKEND_DIR = Path(__file__).resolve().parents[1] os.environ["KEFU_DB_TYPE"] = "sqlite" os.environ["KEFU_DATABASE_URL"] = "" os.environ["KEFU_DB_PATH"] = str(BACKEND_DIR / "kefu.db") if str(BACKEND_DIR) not in sys.path: sys.path.insert(0, str(BACKEND_DIR)) import main def _queue_item(account_id: int, job_id: str = "job-1") -> dict: return { "job_id": job_id, "account_id": account_id, "position": 1, "status": "waiting", "expedited": False, "description": "回复 测试用户", "sender_name": "测试用户", "sender_id": "peer-1", "sender_avatar": None, "conversation_id": "conv-1", "incoming_content": "你好", "incoming_contents": ["你好", "第二条"], "message_count": 2, "replies": ["您好"], "interval_seconds": 60, "enqueued_at": "2026-07-20T10:00:00+00:00", "scheduled_at": "2026-07-20T10:01:00+00:00", "remaining_seconds": 60, } class _FakeService: def __init__(self, account_id: int, items=None, action=None): self.account_id = account_id self._running = True self.items = list(items or []) self.action = action or { "status": "accepted", "job_id": "job-1", "shifted_count": 2, } async def get_reply_queue_snapshot(self): return list(self.items) async def send_queued_reply_now(self, job_id: str): return {**self.action, "job_id": job_id} class ReplyQueueApiTests(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): self.original_workers = main.manager.workers main.manager.workers = {} async def asyncTearDown(self): main.manager.workers = self.original_workers async def test_summary_filters_accounts_by_owner_scope(self): service_one = _FakeService(1, [_queue_item(1)]) service_two = _FakeService(2, [_queue_item(2, "job-2")]) main.manager.workers = { 1: SimpleNamespace(is_running=True, _im_service=service_one), 2: SimpleNamespace(is_running=True, _im_service=service_two), } with patch.object(main, "owned_account_ids", AsyncMock(return_value={1})): response = await main.get_reply_queue_summaries( db=object(), user=SimpleNamespace(id=10, role="operator"), ) self.assertEqual(response.total_pending, 1) self.assertEqual([item.account_id for item in response.items], [1]) async def test_offline_account_detail_returns_empty_snapshot(self): account = SimpleNamespace(id=1, reply_delay_seconds=0) with ( patch.object(main, "get_owned_account", AsyncMock(return_value=account)), patch.object(main, "_global_reply_delay_seconds", return_value=0), ): response = await main.get_account_reply_queue( account_id=1, db=object(), user=SimpleNamespace(id=10, role="operator"), ) self.assertFalse(response.running) self.assertEqual(response.pending_count, 0) self.assertEqual(response.items, []) async def test_detail_preserves_merged_incoming_messages(self): account = SimpleNamespace(id=1, reply_delay_seconds=60) service = _FakeService(1, [_queue_item(1)]) main.manager.workers = { 1: SimpleNamespace(is_running=True, _im_service=service), } with ( patch.object(main, "get_owned_account", AsyncMock(return_value=account)), patch.object(main, "_resolve_effective_reply_delay", return_value=60), ): response = await main.get_account_reply_queue( account_id=1, db=object(), user=SimpleNamespace(id=10, role="operator"), ) self.assertEqual(response.pending_count, 1) self.assertEqual(response.items[0].incoming_contents, ["你好", "第二条"]) self.assertEqual(response.items[0].message_count, 2) self.assertEqual(response.items[0].replies, ["您好"]) async def test_send_now_returns_shifted_count(self): service = _FakeService(1) main.manager.workers = { 1: SimpleNamespace(is_running=True, _im_service=service), } with ( patch.object(main, "get_owned_account", AsyncMock(return_value=SimpleNamespace(id=1))), patch.object(main.system_logger, "record"), ): response = await main.send_account_queued_reply_now( account_id=1, job_id="job-1", db=object(), user=SimpleNamespace(id=10, role="operator"), ) self.assertEqual(response.status, "accepted") self.assertEqual(response.shifted_count, 2) async def test_cross_account_or_finished_job_returns_not_found(self): service = _FakeService(1, action={"status": "not_found"}) main.manager.workers = { 1: SimpleNamespace(is_running=True, _im_service=service), } with patch.object( main, "get_owned_account", AsyncMock(return_value=SimpleNamespace(id=1)), ): with self.assertRaises(main.HTTPException) as caught: await main.send_account_queued_reply_now( account_id=1, job_id="other-account-job", db=object(), user=SimpleNamespace(id=10, role="operator"), ) self.assertEqual(caught.exception.status_code, 404) if __name__ == "__main__": unittest.main()