import unittest from datetime import datetime, timedelta from unittest.mock import patch from app.services.sync_push_service import ( _build_task_pull_backlog_limits, _extract_detect_result_projection_events, _load_latest_projection, _load_pushable_projections, _refresh_remote_runtime_node, _load_local_detect_backlog_snapshot, _push_projection_now, _resolve_task_pull_request_limit, _task_projection_limit, pull_detect_task_batch_now, _resolve_detect_result_target_job_id, _select_relevant_backlog_job_ids_from_rows, _should_throttle_task_pull, ingest_runtime_projection, ) class _FakeCursor: def __init__(self, rows): self._rows = list(rows) self.executed = [] def execute(self, sql, params=None): self.executed.append((sql, params)) def fetchone(self): if self._rows: return self._rows.pop(0) return None def fetchall(self): if self._rows: value = self._rows.pop(0) return list(value or []) return [] def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False class _FakeConnection: def __init__(self, rows): self.cursor_obj = _FakeCursor(rows) self.committed = False def cursor(self): return self.cursor_obj def commit(self): self.committed = True def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False class _FakeUrlopenResponse: def __init__(self, payload): self._payload = payload def read(self): import json return json.dumps(self._payload, ensure_ascii=False).encode("utf-8") def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False class SyncPushServiceTests(unittest.TestCase): @patch("app.services.sync_push_service.cleanup_imported_runtime_nodes_many") @patch("app.services.sync_push_service.cleanup_imported_runtime_nodes") @patch("app.services.sync_push_service.register_node_heartbeat") def test_refresh_remote_runtime_node_imports_cluster_worker_rows_without_active_job( self, mock_register_node_heartbeat, _mock_cleanup_imported_runtime_nodes, mock_cleanup_imported_runtime_nodes_many, ) -> None: _refresh_remote_runtime_node( source_region="mainland", projection={ "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", "hostname": "localhost", "ip": "127.0.0.1", }, "worker_mode": "linux-systemd", "phase_label": "等待中", "phase_detail": "等待任务", "proxy_runtime_label": "正常", "proxy_runtime_reason": "healthy", "active_thread_count": 0, "max_thread_count": 60000, "worker_online": True, "detect_participating": False, "active_job": { "job_code": "", "status": "", "node_stats": [], }, "cluster_nodes": [ { "node_code": "mainland-controller-01", "role": "control", "status": "online", "current_load": 0, "active_threads": 0, "max_threads": 60000, "detect_participating": False, }, { "node_code": "mainland-controller-01-a", "role": "worker", "status": "online", "current_load": 0, "active_threads": 0, "max_threads": 1000, "detect_participating": False, }, { "node_code": "mainland-controller-01-b", "role": "worker", "status": "busy", "current_load": 12, "active_threads": 12, "max_threads": 1000, "detect_participating": True, }, ], }, ) self.assertEqual(3, mock_register_node_heartbeat.call_count) worker_calls = [call.kwargs for call in mock_register_node_heartbeat.call_args_list[1:]] self.assertEqual( ["mainland-controller-01-a", "mainland-controller-01-b"], [item["node_code"] for item in worker_calls], ) self.assertEqual(12, worker_calls[1]["current_load"]) self.assertEqual( ["mainland-controller-01-a", "mainland-controller-01-b"], mock_cleanup_imported_runtime_nodes_many.call_args.kwargs["keep_node_codes"], ) @patch("app.services.sync_push_service.settings") def test_task_pull_request_limit_scales_with_estimated_threads(self, mock_settings) -> None: mock_settings.node_code = "mainland-controller-01" mock_settings.sync_batch_size = 5000 safe_limit = _resolve_task_pull_request_limit( None, settings_payload={ "thread_count": 1000, "process_count": 1, "node_thread_counts": {"mainland-controller-01": 1000}, "node_process_counts": {"mainland-controller-01": 60}, }, ) self.assertEqual(120000, safe_limit) @patch("app.services.sync_push_service.settings") def test_task_projection_limit_allows_large_explicit_pull_request(self, mock_settings) -> None: mock_settings.sync_batch_size = 5000 self.assertEqual(120000, _task_projection_limit(120000)) @patch("app.services.sync_push_service.settings") @patch("app.services.sync_push_service.get_db") def test_load_latest_projection_prefers_non_future_row_over_clock_skewed_history( self, mock_get_db, mock_settings, ) -> None: now = datetime.now() future_row = ( 10081, "mainland", "overseas", "projected", {"projection": {"active_thread_count": 138}}, now + timedelta(hours=4), now + timedelta(hours=4), ) valid_row = ( 11633, "mainland", "overseas", "projected", {"projection": {"active_thread_count": 6431}}, now - timedelta(minutes=2), now - timedelta(minutes=2), ) fake_conn = _FakeConnection(rows=[[future_row, valid_row]]) mock_get_db.return_value = fake_conn mock_settings.sync_source_region = "mainland" mock_settings.node_region = "mainland" mock_settings.sync_target_region = "overseas" with patch("app.services.sync_push_service.datetime") as mock_datetime: mock_datetime.now.return_value = now record = _load_latest_projection("runtime_projection") self.assertIsNotNone(record) self.assertEqual(11633, record["id"]) executed_sql, executed_params = fake_conn.cursor_obj.executed[0] self.assertIn("CASE WHEN created_at <= %s THEN 0 ELSE 1 END", executed_sql) self.assertEqual("runtime_projection", executed_params[0]) self.assertEqual("mainland", executed_params[1]) self.assertEqual("overseas", executed_params[2]) self.assertEqual(now + timedelta(minutes=5), executed_params[3]) @patch("app.services.sync_push_service._latest_push_attempt") @patch("app.services.sync_push_service.settings") @patch("app.services.sync_push_service.get_db") def test_load_pushable_projections_prefers_latest_unsent_records( self, mock_get_db, mock_settings, mock_latest_push_attempt, ) -> None: now = datetime.now() old_success_rows = [ ( 10000 + index, "mainland", "overseas", "projected", {"projection_hash": f"old-{index}", "projection": {"job": {"job_id": index}}}, now - timedelta(minutes=120 - index), now - timedelta(minutes=120 - index), ) for index in range(100) ] newest_unsent = ( 23360, "mainland", "overseas", "projected", {"projection_hash": "new-hash", "projection": {"job": {"job_id": 909, "job_code": "sync-overseas-28618"}}}, now, now, ) fake_conn = _FakeConnection(rows=[old_success_rows + [newest_unsent]]) mock_get_db.return_value = fake_conn mock_settings.sync_source_region = "mainland" mock_settings.node_region = "mainland" mock_settings.sync_target_region = "overseas" mock_settings.sync_batch_size = 20 def _attempt_side_effect(source_record_id, target_region, sync_type): if source_record_id == 23360: return None return {"status": "success", "created_at": now} mock_latest_push_attempt.side_effect = _attempt_side_effect records = _load_pushable_projections("detect_result_projection", limit=20) self.assertEqual([23360], [item["id"] for item in records]) executed_sql, _ = fake_conn.cursor_obj.executed[0] self.assertIn("ORDER BY created_at DESC, id DESC", executed_sql) @patch("app.services.sync_push_service.settings.node_code", "mainland-controller-01") def test_build_task_pull_backlog_limits_scales_with_local_server_capacity(self) -> None: limits = _build_task_pull_backlog_limits( 5000, settings_payload={ "thread_count": 100, "process_count": 80, "node_thread_counts": { "mainland-controller-01": 1000, "mainland-controller-01-a": 1000, "mainland-worker-01": 1200, }, "node_process_counts": { "mainland-controller-01": 80, "mainland-worker-01": 60, }, }, ) self.assertEqual(80000, limits["estimated_total_threads"]) self.assertEqual(160000, limits["max_pending_total"]) self.assertEqual(80000, limits["max_register_pending"]) self.assertEqual(20000, limits["max_downstream_pending"]) def test_select_relevant_backlog_job_ids_from_rows_skips_stale_pending_jobs(self) -> None: now = datetime.now() job_ids = _select_relevant_backlog_job_ids_from_rows( [ (11, "running", now - timedelta(hours=10)), (10, "pending", now - timedelta(minutes=30)), (9, "pending", now - timedelta(hours=7)), ], freshness_hours=6, limit=4, ) self.assertEqual([11, 10], job_ids) @patch("app.services.sync_push_service.get_active_detect_job_summary") @patch("app.services.sync_push_service.get_db") def test_load_local_detect_backlog_snapshot_prefers_active_job_snapshot( self, mock_get_db, mock_get_active_detect_job_summary, ) -> None: mock_get_active_detect_job_summary.return_value = { "items_pending": 123, "items_claimed": 4, "items_running": 7, "display_items_running": 9, "step_stats": [ {"step_code": "detect_register", "items_pending": 90}, {"step_code": "detect_baidu", "items_pending": 33}, ], } backlog = _load_local_detect_backlog_snapshot() self.assertEqual(123, backlog["pending_total"]) self.assertEqual(4, backlog["claimed_total"]) self.assertEqual(9, backlog["running_total"]) self.assertEqual(90, backlog["register_pending"]) self.assertEqual(33, backlog["downstream_pending"]) mock_get_db.assert_not_called() @patch("app.services.sync_push_service.get_active_detect_job_summary", return_value={}) @patch("app.services.sync_push_service.get_db") def test_load_local_detect_backlog_snapshot_only_counts_relevant_jobs( self, mock_get_db, _mock_get_active_detect_job_summary, ) -> None: fake_conn = _FakeConnection( rows=[ [ (11, "running", datetime.now() - timedelta(hours=2)), (10, "pending", datetime.now() - timedelta(minutes=30)), (9, "pending", datetime.now() - timedelta(hours=7)), ], (123, 4, 7, 90, 33), ] ) mock_get_db.return_value = fake_conn backlog = _load_local_detect_backlog_snapshot() self.assertEqual(123, backlog["pending_total"]) self.assertEqual(90, backlog["register_pending"]) executed_sql, executed_params = fake_conn.cursor_obj.executed[1] self.assertIn("WHERE item.job_id = ANY(%s)", executed_sql) self.assertEqual([11, 10], list(executed_params[0])) def test_should_throttle_task_pull_when_register_backlog_overwhelms_downstream(self) -> None: should_throttle, reason = _should_throttle_task_pull( { "pending_total": 9438, "claimed_total": 410, "running_total": 15, "register_pending": 8487, "downstream_pending": 951, }, { "max_pending_total": 12000, "max_register_pending": 3200, "max_downstream_pending": 800, }, ) self.assertTrue(should_throttle) self.assertEqual("register_pending", reason) def test_should_not_throttle_task_pull_when_backlog_is_within_limits(self) -> None: should_throttle, reason = _should_throttle_task_pull( { "pending_total": 1800, "claimed_total": 100, "running_total": 300, "register_pending": 600, "downstream_pending": 120, }, { "max_pending_total": 6400, "max_register_pending": 3200, "max_downstream_pending": 800, }, ) self.assertFalse(should_throttle) self.assertEqual("", reason) @patch("app.services.sync_push_service._should_throttle_task_pull", return_value=(False, "")) @patch("app.services.sync_push_service._build_task_pull_backlog_limits", return_value={}) @patch("app.services.sync_push_service._load_local_detect_backlog_snapshot", return_value={}) @patch("app.services.sync_push_service._acquire_sync_pull_worker_wake_guard", return_value=True) @patch("app.services.sync_push_service.ingest_detect_task_projection") @patch("app.services.worker_control_service.send_worker_command") @patch("app.services.sync_push_service.urllib.request.urlopen") @patch("app.services.sync_push_service.settings") @patch("app.services.sync_push_service.get_settings_payload") def test_pull_detect_task_batch_now_starts_worker_with_projection_active_job_identity( self, mock_get_settings_payload, mock_settings, mock_urlopen, mock_send_worker_command, mock_ingest_detect_task_projection, _mock_acquire_sync_pull_worker_wake_guard, _mock_load_local_detect_backlog_snapshot, _mock_build_task_pull_backlog_limits, _mock_should_throttle_task_pull, ) -> None: mock_settings.node_region = "mainland" mock_settings.node_role = "control" mock_settings.node_code = "mainland-controller-01" mock_settings.sync_target_api_base_url = "http://example.com" mock_settings.sync_target_region = "overseas" mock_settings.sync_batch_size = 200 mock_settings.sync_shared_token = "" mock_get_settings_payload.return_value = { "thread_count": 1000, "process_count": 1, "node_thread_counts": {"mainland-controller-01": 1000}, "node_process_counts": {"mainland-controller-01": 60}, } projection = { "batch_code": "task-20260423210000-aa11bb", "active_job": { "job_id": 376, "job_code": "sync-overseas-376", "current_cycle_token": "cycle-376", }, } export_response = { "code": 0, "data": { "source_record_id": 15164, "projection_hash": "hash-15164", "projection": projection, }, } ack_response = {"code": 0, "data": {"acknowledged": True}} mock_urlopen.side_effect = [ _FakeUrlopenResponse(export_response), _FakeUrlopenResponse(ack_response), ] mock_ingest_detect_task_projection.return_value = ( True, "任务批次接收成功", { "target_job_id": 1902, "target_job_code": "sync-overseas-15164", "queued_count": 1000, }, ) mock_send_worker_command.return_value = (True, "started") ok, message, payload = pull_detect_task_batch_now(limit=1000) self.assertTrue(ok) self.assertEqual("待检测任务批次拉取并入库成功", message) self.assertTrue(payload["worker_start_ok"]) export_request = mock_urlopen.call_args_list[0][0][0] self.assertIn("limit=1000", export_request.full_url) mock_send_worker_command.assert_called_once() args, kwargs = mock_send_worker_command.call_args self.assertEqual("start_detection", args[0]) sent_payload = kwargs["payload"] self.assertEqual(15164, sent_payload["source_record_id"]) self.assertEqual(1902, sent_payload["target_job_id"]) self.assertEqual("sync-overseas-15164", sent_payload["target_job_code"]) self.assertEqual(376, sent_payload["job_id"]) self.assertEqual("sync-overseas-376", sent_payload["job_code"]) self.assertEqual("cycle-376", sent_payload["cycle_token"]) @patch("app.services.sync_push_service._should_throttle_task_pull", return_value=(False, "")) @patch("app.services.sync_push_service._build_task_pull_backlog_limits", return_value={}) @patch("app.services.sync_push_service._load_local_detect_backlog_snapshot", return_value={}) @patch("app.services.sync_push_service._acquire_sync_pull_worker_wake_guard", return_value=False) @patch("app.services.sync_push_service.ingest_detect_task_projection") @patch("app.services.worker_control_service.send_worker_command") @patch("app.services.sync_push_service.urllib.request.urlopen") @patch("app.services.sync_push_service.settings") @patch("app.services.sync_push_service.get_settings_payload") def test_pull_detect_task_batch_now_skips_duplicate_worker_wake_within_short_window( self, mock_get_settings_payload, mock_settings, mock_urlopen, mock_send_worker_command, mock_ingest_detect_task_projection, _mock_acquire_sync_pull_worker_wake_guard, _mock_load_local_detect_backlog_snapshot, _mock_build_task_pull_backlog_limits, _mock_should_throttle_task_pull, ) -> None: mock_settings.node_region = "mainland" mock_settings.node_role = "control" mock_settings.node_code = "mainland-controller-01" mock_settings.sync_target_api_base_url = "http://example.com" mock_settings.sync_target_region = "overseas" mock_settings.sync_batch_size = 200 mock_settings.sync_shared_token = "" mock_get_settings_payload.return_value = { "thread_count": 1000, "process_count": 1, "node_thread_counts": {"mainland-controller-01": 1000}, "node_process_counts": {"mainland-controller-01": 60}, } projection = { "batch_code": "task-20260423210000-aa11bb", "active_job": { "job_id": 376, "job_code": "sync-overseas-376", "current_cycle_token": "cycle-376", }, } export_response = { "code": 0, "data": { "source_record_id": 15164, "projection_hash": "hash-15164", "projection": projection, }, } ack_response = {"code": 0, "data": {"acknowledged": True}} mock_urlopen.side_effect = [ _FakeUrlopenResponse(export_response), _FakeUrlopenResponse(ack_response), ] mock_ingest_detect_task_projection.return_value = ( True, "任务批次接收成功", { "target_job_id": 1902, "target_job_code": "sync-overseas-15164", "queued_count": 1000, }, ) ok, message, payload = pull_detect_task_batch_now(limit=1000) self.assertTrue(ok) self.assertEqual("待检测任务批次拉取并入库成功", message) self.assertTrue(payload["worker_start_ok"]) self.assertTrue(payload["worker_start_skipped"]) self.assertIn("重复 Worker 唤起", payload["worker_start_message"]) mock_send_worker_command.assert_not_called() @patch("app.services.sync_push_service._should_throttle_task_pull", return_value=(False, "")) @patch("app.services.sync_push_service._build_task_pull_backlog_limits", return_value={}) @patch("app.services.sync_push_service._load_local_detect_backlog_snapshot", return_value={}) @patch("app.services.sync_push_service._acquire_sync_pull_worker_wake_guard", return_value=False) @patch("app.services.sync_push_service.ingest_detect_task_projection") @patch("app.services.sync_push_service.urllib.request.urlopen") @patch("app.services.sync_push_service.settings") @patch("app.services.sync_push_service.get_settings_payload") def test_pull_detect_task_batch_now_uses_adaptive_limit_when_unspecified( self, mock_get_settings_payload, mock_settings, mock_urlopen, mock_ingest_detect_task_projection, _mock_acquire_sync_pull_worker_wake_guard, _mock_load_local_detect_backlog_snapshot, _mock_build_task_pull_backlog_limits, _mock_should_throttle_task_pull, ) -> None: mock_settings.node_region = "mainland" mock_settings.node_role = "control" mock_settings.node_code = "mainland-controller-01" mock_settings.sync_target_api_base_url = "http://example.com" mock_settings.sync_target_region = "overseas" mock_settings.sync_batch_size = 5000 mock_settings.sync_shared_token = "" mock_get_settings_payload.return_value = { "thread_count": 1000, "process_count": 1, "node_thread_counts": {"mainland-controller-01": 1000}, "node_process_counts": {"mainland-controller-01": 60}, } export_response = { "code": 0, "data": { "source_record_id": 15164, "projection_hash": "hash-15164", "projection": {"batch_code": "task-20260423210000-aa11bb", "active_job": {}}, }, } ack_response = {"code": 0, "data": {"acknowledged": True}} mock_urlopen.side_effect = [ _FakeUrlopenResponse(export_response), _FakeUrlopenResponse(ack_response), ] mock_ingest_detect_task_projection.return_value = ( True, "任务批次接收成功", {"target_job_id": 1902, "target_job_code": "sync-overseas-15164", "queued_count": 1000}, ) ok, _message, _payload = pull_detect_task_batch_now(limit=None) self.assertTrue(ok) export_request = mock_urlopen.call_args_list[0][0][0] self.assertIn("limit=120000", export_request.full_url) def test_extract_detect_result_projection_events_adds_import_metadata(self) -> None: projection = { "job": { "job_id": 12, "job_code": "detect-local-001", }, "recent_domain_events": [ { "node_code": "mainland-worker-01", "event_type": "domain_completed", "level": "info", "message": "完成 a.com", "created_at": "2026-04-19 01:05:00", "payload": {"domain": "a.com", "domain_id": 101, "cycle_token": "cycle-1"}, } ], } events = _extract_detect_result_projection_events( source_region="mainland", source_record_id=88, projection=projection, ) self.assertEqual(1, len(events)) event = events[0] self.assertEqual("domain_completed", event["event_type"]) self.assertEqual("mainland", event["payload"]["import_source_region"]) self.assertEqual(88, event["payload"]["import_source_record_id"]) self.assertEqual("detect-local-001", event["payload"]["import_source_job_code"]) self.assertTrue(event["payload"]["imported_from_projection"]) self.assertTrue(event["payload"]["import_fingerprint"]) @patch("app.services.sync_push_service._push_projection_record") @patch("app.services.sync_push_service._load_latest_projection") @patch("app.services.runtime_status_service.refresh_runtime_projection_snapshot") def test_push_projection_now_refreshes_runtime_projection_with_lightweight_snapshot( self, mock_refresh_runtime_projection_snapshot, mock_load_latest_projection, mock_push_projection_record, ) -> None: mock_refresh_runtime_projection_snapshot.return_value = { "record_id": 10082, "active_thread_count": 1972, "max_thread_count": 80000, "queue_display_running": 1972, } mock_load_latest_projection.return_value = { "id": 10082, "source_region": "mainland", "target_region": "overseas", "created_at": None, "payload": {"projection_hash": "hash-10082", "projection": {}}, } mock_push_projection_record.return_value = ( True, "投影推送成功", {"action": "push_sync", "sync_type": "runtime_projection", "source_record_id": 10082}, ) ok, message, data = _push_projection_now("runtime_projection", "https://example.com/api/v1/runtime/sync-ingest") self.assertTrue(ok) self.assertEqual("投影推送成功", message) self.assertEqual(10082, data["source_record_id"]) mock_refresh_runtime_projection_snapshot.assert_called_once_with(window_minutes=15) mock_load_latest_projection.assert_called_once_with("runtime_projection") mock_push_projection_record.assert_called_once() @patch("app.services.sync_push_service._push_projection_record") @patch("app.services.sync_push_service._load_latest_projection") @patch("app.services.sync_push_service._append_fast_runtime_projection_snapshot") @patch("app.services.runtime_status_service.refresh_runtime_projection_snapshot") def test_push_projection_now_can_use_fast_runtime_projection_path( self, mock_refresh_runtime_projection_snapshot, mock_append_fast_runtime_projection_snapshot, mock_load_latest_projection, mock_push_projection_record, ) -> None: mock_append_fast_runtime_projection_snapshot.return_value = 10091 mock_load_latest_projection.return_value = { "id": 10091, "source_region": "mainland", "target_region": "overseas", "created_at": None, "payload": {"projection_hash": "hash-10091", "projection": {}}, } mock_push_projection_record.return_value = ( True, "投影推送成功", {"action": "push_sync", "sync_type": "runtime_projection", "source_record_id": 10091}, ) with patch.dict("os.environ", {"DOMAINCHECK_SYNC_RUNTIME_FAST_PROJECTION": "1"}, clear=False): ok, message, data = _push_projection_now("runtime_projection", "https://example.com/api/v1/runtime/sync-ingest") self.assertTrue(ok) self.assertEqual("投影推送成功", message) self.assertEqual(10091, data["source_record_id"]) mock_append_fast_runtime_projection_snapshot.assert_called_once() mock_refresh_runtime_projection_snapshot.assert_not_called() mock_load_latest_projection.assert_called_once_with("runtime_projection") mock_push_projection_record.assert_called_once() @patch("app.services.sync_push_service.get_db") def test_resolve_detect_result_target_job_id_prefers_matching_job_code(self, mock_get_db) -> None: fake_conn = _FakeConnection(rows=[(456,)]) mock_get_db.return_value = fake_conn target_job_id = _resolve_detect_result_target_job_id( projection={ "job": { "job_code": "sync-mainland-123", } } ) self.assertEqual(456, target_job_id) self.assertIn("WHERE job_code = %s", fake_conn.cursor_obj.executed[0][0]) @patch("app.services.detect_job_service.get_active_detect_job_summary") @patch("app.services.sync_push_service.get_db") def test_resolve_detect_result_target_job_id_falls_back_to_active_job( self, mock_get_db, mock_get_active_detect_job_summary, ) -> None: fake_conn = _FakeConnection(rows=[None]) mock_get_db.return_value = fake_conn mock_get_active_detect_job_summary.return_value = {"job_id": 789} target_job_id = _resolve_detect_result_target_job_id( projection={ "job": { "job_code": "sync-mainland-999", } } ) self.assertEqual(789, target_job_id) @patch("app.services.sync_push_service._import_detect_result_projection_events") @patch("app.services.sync_push_service.get_db") def test_ingest_runtime_projection_imports_detect_result_events_on_new_record( self, mock_get_db, mock_import_events, ) -> None: fake_conn = _FakeConnection(rows=[None, (321,)]) mock_get_db.return_value = fake_conn mock_import_events.return_value = {"imported_count": 2, "deduplicated_count": 0, "target_job_id": 9} ok, message, data = ingest_runtime_projection( { "sync_type": "detect_result_projection", "source_region": "mainland", "source_record_id": 66, "projection_hash": "hash-001", "projection": {"recent_domain_events": []}, } ) self.assertTrue(ok) self.assertEqual("同步投影接收成功", message) self.assertEqual(321, data["record_id"]) self.assertEqual(2, data["event_import"]["imported_count"]) mock_import_events.assert_called_once_with( source_region="mainland", source_record_id=66, projection={"recent_domain_events": []}, ) self.assertTrue(fake_conn.committed) @patch("app.services.sync_push_service._import_detect_result_projection_events") @patch("app.services.sync_push_service.get_db") def test_ingest_runtime_projection_imports_detect_result_events_on_deduplicated_record( self, mock_get_db, mock_import_events, ) -> None: fake_conn = _FakeConnection(rows=[(777,)]) mock_get_db.return_value = fake_conn mock_import_events.return_value = {"imported_count": 0, "deduplicated_count": 3, "target_job_id": 9} ok, message, data = ingest_runtime_projection( { "sync_type": "detect_result_projection", "source_region": "mainland", "source_record_id": 66, "projection_hash": "hash-001", "projection": {"recent_domain_events": []}, } ) self.assertTrue(ok) self.assertEqual("同步投影已存在,已按幂等处理", message) self.assertTrue(data["deduplicated"]) self.assertEqual(3, data["event_import"]["deduplicated_count"]) mock_import_events.assert_called_once_with( source_region="mainland", source_record_id=66, projection={"recent_domain_events": []}, ) if __name__ == "__main__": unittest.main()