import unittest from unittest.mock import patch from app.services.dashboard import fetch_overview class _FakeCursor: def __init__(self, responses): self._responses = list(responses) def execute(self, sql, params=None): self._last_sql = sql self._last_params = params def fetchone(self): if self._responses: return self._responses.pop(0) return (0,) def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False class _FakeConnection: def __init__(self, responses): self._cursor = _FakeCursor(responses) def cursor(self): return self._cursor def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False class DashboardServiceTests(unittest.TestCase): @patch("app.services.dashboard._fetch_active_jobs_aggregate") @patch("app.services.dashboard.get_detect_capacity_plan") @patch("app.services.dashboard.get_detect_queue_health") @patch("app.services.dashboard.get_runtime_status") @patch("app.services.dashboard.get_active_detect_job_summary") @patch("app.services.dashboard.get_db") def test_fetch_overview_includes_ops_metrics( self, mock_get_db, mock_get_active_detect_job_summary, mock_get_runtime_status, mock_get_detect_queue_health, mock_get_detect_capacity_plan, mock_fetch_active_jobs_aggregate, ) -> None: mock_get_db.return_value = _FakeConnection( responses=[ (1000,), (900,), (10,), (5,), (0,), (1,), (430,), (420,), (17,), ] ) mock_fetch_active_jobs_aggregate.return_value = { "active_jobs_total": 3, "queue": { "items_total": 5200, "pending": 4700, "claimed": 11, "running": 165, "completed": 300, "blacklisted": 12, "failed": 7, }, "throughput": { "processed_recent": 30, "processed_per_minute": 2.0, "completed_recent": 20, "blacklisted_recent": 1, "failed_recent": 2, }, "retry_total": 12, "steps": [ { "step_code": "detect_register", "step_name": "注册状态检测", "items_pending": 185, "items_running": 3, "items_claimed": 0, "items_completed": 7, "items_blacklisted": 0, "items_failed": 0, "processed_recent": 20, "processed_per_minute": 1.33, } ], "nodes": [ { "node_code": "mainland-controller-01", "items_running": 120, "items_claimed": 0, "processed_recent": 20, "processed_per_minute": 1.33, "completed_recent": 15, "failed_recent": 1, "blacklisted_recent": 0, } ], } mock_get_active_detect_job_summary.return_value = { "job_id": 76, "job_code": "sync-overseas-3612", "items_pending": 185, "items_running": 8, "items_completed": 7, "items_blacklisted": 0, "items_failed": 0, } mock_get_runtime_status.return_value = { "worker": {"running": True, "mode": "linux-systemd", "expected_on_this_node": True}, "node": {"region": "overseas", "role": "control"}, "cluster": {"summary": {"online_worker_nodes": 2, "dedicated_online_worker_nodes": 1, "online_control_nodes": 1}}, "detect": { "backlog": { "pending_total": 9438, "claimed_total": 410, "running_total": 15, "completed_total": 951, "blacklisted_total": 0, "failed_total": 137, "register_pending": 8487, "downstream_pending": 951, } }, } mock_get_detect_queue_health.return_value = { "has_active_job": True, "job": {"job_id": 76, "job_code": "sync-overseas-3612", "status": "running", "progress_percent": 3.5}, "queue": { "items_total": 200, "pending": 185, "claimed": 0, "display_claimed": 0, "running": 8, "display_running": 165, "completed": 7, "blacklisted": 0, "failed": 0, }, "throughput": {"processed_recent": 0, "processed_per_minute": 0}, "steps": [ { "step_code": "detect_register", "step_name": "注册状态检测", "items_pending": 185, "items_running": 165, "items_claimed": 0, "items_completed": 7, "items_blacklisted": 0, "items_failed": 0, "processed_recent": 0, "processed_per_minute": 0.0, } ], "runtime_activity": { "step_stats": { "detect_360_site": { "step_code": "detect_360_site", "started_recent": 269, "processed_recent": 6, "completed_recent": 6, "failed_recent": 0, "blacklisted_recent": 0, } } }, "nodes": [ { "node_code": "mainland-controller-01", "items_running": 165, "items_claimed": 0, "processed_recent": 0, "processed_per_minute": 0.0, "completed_recent": 0, "failed_recent": 0, "blacklisted_recent": 0, } ], } mock_get_detect_capacity_plan.return_value = { "estimated_hours_remaining": 1.5, "remaining_items": 193, "recommended_additional_workers": 1, } data = fetch_overview() self.assertEqual("sync-overseas-3612", data["active_job"]["job_code"]) self.assertEqual(3, data["active_jobs_aggregate"]["active_jobs_total"]) self.assertEqual(9438, data["active_jobs_aggregate"]["queue"]["pending"]) self.assertEqual(410, data["active_jobs_aggregate"]["queue"]["claimed"]) self.assertEqual(15, data["active_jobs_aggregate"]["queue"]["running"]) self.assertEqual(951, data["active_jobs_aggregate"]["queue"]["completed"]) self.assertEqual(137, data["active_jobs_aggregate"]["queue"]["failed"]) self.assertEqual("mainland-controller-01", data["active_jobs_aggregate"]["nodes"][0]["node_code"]) self.assertEqual(900, data["pending_total"]) self.assertEqual(10, data["completed_total"]) self.assertEqual(430, data["registerable_total"]) self.assertEqual(420, data["purchasable_total"]) self.assertEqual(185, data["queue_pending_total"]) self.assertEqual(8, data["queue_running_total"]) self.assertEqual(165, data["queue_display_running_total"]) self.assertEqual(7, data["queue_completed_total"]) self.assertEqual(9438, data["backlog_pending_total"]) self.assertEqual(8487, data["backlog_register_pending_total"]) self.assertEqual(951, data["backlog_downstream_pending_total"]) self.assertEqual(12, data["retry_total"]) self.assertEqual("注册状态检测", data["bottleneck_step"]["step_name"]) self.assertEqual(0.0, data["processed_per_minute"]) self.assertEqual(0, data["processed_recent"]) self.assertEqual(0.0, data["ops_summary"]["processed_per_minute"]) self.assertEqual(1.5, data["ops_summary"]["estimated_hours_remaining"]) self.assertEqual(1, data["active_execution_nodes"]) self.assertEqual(1, data["ops_summary"]["active_execution_nodes"]) self.assertEqual(2, len(data["step_queue"])) self.assertTrue(any(item["step_code"] == "detect_360_site" for item in data["step_queue"])) self.assertEqual(1, len(data["node_throughput"])) if __name__ == "__main__": unittest.main()