import unittest from datetime import datetime, timedelta from unittest.mock import MagicMock, patch from psycopg2 import errors import app.services.detect_job_service as detect_job_service_module from app.services.detect_job_service import ( append_detect_job_event, _build_active_job_summary_from_runtime_snapshot, _build_step_payload, _classify_pipeline_item_outcome, _classify_runtime_debug_event, _build_runtime_snapshot_from_projection, _enrich_active_job_summary_with_runtime, _build_display_summary, _build_runtime_display_bucket, _load_latest_runtime_active_job_snapshot, get_active_detect_job_summary, normalize_detect_step_code, get_detect_queue_health, process_detect_pipeline_now, resolve_initial_domain_pipeline_item, resolve_domain_pipeline_step, resolve_detect_job_definition, ) class _SequenceCursor: def __init__(self, *, fetchone_results=None, fetchall_results=None) -> None: self.fetchone_results = list(fetchone_results or []) self.fetchall_results = list(fetchall_results or []) self.exec_calls = [] def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False def execute(self, sql, params=None): self.exec_calls.append((sql, params)) def fetchone(self): if self.fetchone_results: return self.fetchone_results.pop(0) return None def fetchall(self): if self.fetchall_results: return self.fetchall_results.pop(0) return [] class _SequenceConn: def __init__(self, *, fetchone_results=None, fetchall_results=None) -> None: self.cursor_obj = _SequenceCursor(fetchone_results=fetchone_results, fetchall_results=fetchall_results) def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False def cursor(self): return self.cursor_obj class DetectJobServiceTests(unittest.TestCase): def test_load_runtime_display_rows_excludes_disabled_managed_nodes(self) -> None: cursor = MagicMock() cursor.fetchall.return_value = [ ("mainland-controller-01", "mainland", "control", "busy", 1, {}, datetime.now()), ("mainland-worker-01", "mainland", "worker", "busy", 1, {}, datetime.now()), ] with patch("app.services.detect_job_service._load_disabled_managed_node_codes", return_value={"mainland-worker-01"}): rows = detect_job_service_module._load_runtime_display_rows(cursor) self.assertEqual(["mainland-controller-01"], [row[0] for row in rows]) def test_build_active_job_summary_from_runtime_snapshot_keeps_non_zero_display_when_queue_display_is_zero(self) -> None: with patch("app.services.detect_job_service.settings.node_region", "mainland"): summary = _build_active_job_summary_from_runtime_snapshot( { "job": { "job_id": 60, "job_code": "sync-overseas-255", "status": "running", "items_total": 8402, "items_pending": 7865, "items_claimed": 0, "items_running": 537, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "items_terminal": 0, "display_items_running": 0, "display_active_threads": 0, "display_max_threads": 0, "node_stats": [ { "node_code": "mainland-controller-01-a", "items_running": 0, "display_running": 0, "active_threads": 0, "max_threads": 0, } ], }, "queue_health": { "queue": { "items_total": 8402, "pending": 7865, "claimed": 0, "running": 537, "display_running": 0, "completed": 0, "blacklisted": 0, "failed": 0, "terminal": 0, }, "nodes": [ { "node_code": "mainland-controller-01-a", "items_running": 0, "display_running": 0, "active_threads": 0, "max_threads": 0, } ], "steps": [], "throughput": {}, }, "_snapshot_source": "runtime_ingest", } ) assert summary is not None self.assertEqual(537, summary["display_items_running"]) self.assertEqual(537, summary["display_active_threads"]) self.assertEqual(537, summary["display_current_load"]) def test_build_active_job_summary_from_runtime_snapshot_drops_stale_runtime_nodes_from_live_display(self) -> None: stale_heartbeat = (datetime.now() - timedelta(minutes=8)).isoformat(sep=" ", timespec="seconds") snapshot_created_at = datetime.now().isoformat(sep=" ", timespec="seconds") summary = _build_active_job_summary_from_runtime_snapshot( { "job": { "job_id": 61, "job_code": "sync-overseas-256", "status": "running", "items_total": 8402, "items_pending": 7865, "items_claimed": 0, "items_running": 537, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "items_terminal": 0, "display_items_running": 537, "display_active_threads": 537, "node_stats": [ { "node_code": "mainland-controller-01-a", "items_running": 537, "display_running": 537, "active_threads": 537, "max_threads": 1000, "status": "busy", "last_heartbeat_at": stale_heartbeat, } ], }, "queue_health": { "queue": { "items_total": 8402, "pending": 7865, "claimed": 0, "running": 537, "display_running": 537, "completed": 0, "blacklisted": 0, "failed": 0, "terminal": 0, }, "nodes": [ { "node_code": "mainland-controller-01-a", "items_running": 537, "display_running": 537, "active_threads": 537, "max_threads": 1000, "status": "busy", "last_heartbeat_at": stale_heartbeat, } ], "steps": [], "throughput": {}, }, "_snapshot_source": "runtime_ingest", "_created_at": snapshot_created_at, } ) assert summary is not None self.assertEqual(537, summary["items_running"]) self.assertEqual(0, summary["display_items_running"]) self.assertEqual(0, summary["display_active_threads"]) self.assertEqual([], summary["node_stats"]) def test_build_active_job_summary_from_runtime_snapshot_does_not_promote_unassigned_runtime_load(self) -> None: with patch("app.services.detect_job_service.settings.node_region", "mainland"): summary = _build_active_job_summary_from_runtime_snapshot( { "job": { "job_id": 62, "job_code": "sync-overseas-257", "status": "running", "items_total": 4000, "items_pending": 0, "items_claimed": 23, "items_running": 2100, "items_completed": 1117, "items_blacklisted": 0, "items_failed": 760, "items_terminal": 1877, "display_items_running": 0, "display_active_threads": 0, "display_max_threads": 0, "node_stats": [ { "node_code": "unassigned", "items_claimed": 23, "items_running": 1863, "display_running": 1863, "active_threads": 0, "max_threads": 0, } ], }, "queue_health": { "queue": { "items_total": 4000, "pending": 0, "claimed": 23, "running": 2100, "display_running": 2100, "completed": 1117, "blacklisted": 0, "failed": 760, "terminal": 1877, }, "nodes": [ { "node_code": "unassigned", "items_claimed": 23, "items_running": 1863, "display_running": 1863, "active_threads": 0, "max_threads": 0, } ], "steps": [], "throughput": {}, }, "_snapshot_source": "runtime_ingest", } ) assert summary is not None self.assertEqual(0, summary["display_items_running"]) self.assertEqual(0, summary["display_active_threads"]) self.assertEqual([], summary["display_active_node_codes"]) def test_append_detect_job_event_skips_missing_job_fk(self) -> None: class FakeCursor: def __init__(self) -> None: self.exec_calls = [] self.selects = 0 def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False def execute(self, sql, params=None): self.exec_calls.append((sql, params)) if "INSERT INTO detect_run_events" in sql: raise AssertionError("should not insert detect_run_events when job row is missing") if "SELECT 1 FROM detect_jobs" in sql: self.selects += 1 def fetchone(self): return None class FakeConn: def __init__(self) -> None: self.cursor_obj = FakeCursor() self.commit_calls = 0 def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return False def cursor(self): return self.cursor_obj def commit(self): self.commit_calls += 1 fake_conn = FakeConn() with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch("app.services.detect_job_service.push_debug_event") as mock_push: append_detect_job_event( 5, event_type="job_dispatch_requested", message="控制面已发送检测启动请求", ) self.assertEqual(1, fake_conn.cursor_obj.selects) self.assertEqual(0, fake_conn.commit_calls) mock_push.assert_called_once() def test_process_detect_pipeline_now_retries_deadlock_once(self) -> None: calls = {"count": 0} def _run_pipeline(*, limit, job_id): calls["count"] += 1 if calls["count"] == 1: raise errors.DeadlockDetected() return { "processed_items": 7, "advanced_items": 3, "retried_items": 1, } with patch("app.services.detect_job_service.process_detect_pipeline", side_effect=_run_pipeline): with patch("app.services.detect_job_service.time.sleep") as mock_sleep: ok, message, data = process_detect_pipeline_now(limit=123, job_id=45) self.assertTrue(ok) self.assertIn("deadlock 自动重试 1 次后成功", message) self.assertEqual(1, data["retry_attempts"]) self.assertEqual(7, data["processed_items"]) self.assertEqual(2, calls["count"]) mock_sleep.assert_called_once() def test_build_runtime_snapshot_from_projection_prefers_distributed_projection_nodes(self) -> None: snapshot = _build_runtime_snapshot_from_projection( { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 1972, "max_thread_count": 2000, "progress": { "pending": 4578, "running": 1972, "completed": 690, "blacklisted": 0, "failed": 24, }, "active_job": { "job_id": 55, "job_code": "sync-overseas-55", "status": "running", "progress_percent": 13.49, "items_total": 5292, "items_terminal": 714, "items_pending": 4578, "items_running": 1972, "items_failed": 24, "display_items_claimed": 120, "display_items_running": 1972, "display_active_threads": 1972, "display_max_threads": 2000, "distributed_node_stats": [ { "node_code": "mainland-controller-01-a", "items_running": 1000, "display_running": 1000, "active_threads": 1000, "max_threads": 1000, "region": "mainland", "role": "control", "status": "busy", }, { "node_code": "mainland-controller-01-b", "items_running": 972, "display_running": 972, "active_threads": 972, "max_threads": 1000, "region": "mainland", "role": "control", "status": "busy", }, ], }, } ) queue = snapshot["queue_health"]["queue"] nodes = snapshot["queue_health"]["nodes"] self.assertEqual(1972, queue["running"]) self.assertEqual(1972, queue["display_running"]) self.assertEqual(3, len(nodes)) self.assertEqual("mainland-controller-01-a", nodes[0]["node_code"]) self.assertEqual(1000, nodes[0]["display_running"]) self.assertEqual(972, nodes[1]["active_threads"]) self.assertEqual("unassigned", nodes[2]["node_code"]) def test_enrich_active_job_summary_with_runtime_does_not_overwrite_non_zero_display_with_zero_snapshot(self) -> None: summary = _enrich_active_job_summary_with_runtime( { "job_id": 60, "job_code": "sync-overseas-255", "status": "running", "items_total": 8402, "items_pending": 7865, "items_claimed": 0, "items_running": 537, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "items_terminal": 0, "display_items_running": 537, "display_active_threads": 537, "display_current_load": 537, "display_max_threads": 80000, "recent_events": [], }, runtime_activity={}, runtime_snapshot={ "job": { "job_id": 60, "job_code": "sync-overseas-255", "status": "running", "node_stats": [ { "node_code": "mainland-controller-01-a", "items_running": 0, "display_running": 0, "active_threads": 0, "max_threads": 0, } ], }, "queue_health": { "queue": { "items_total": 8402, "pending": 7865, "claimed": 0, "running": 537, "display_running": 0, "completed": 0, "blacklisted": 0, "failed": 0, "terminal": 0, }, "nodes": [ { "node_code": "mainland-controller-01-a", "items_running": 0, "display_running": 0, "active_threads": 0, "max_threads": 0, } ], "steps": [], }, }, ) assert summary is not None self.assertEqual(537, summary["display_items_running"]) self.assertEqual(537, summary["display_active_threads"]) self.assertEqual(80000, summary["display_max_threads"]) def test_classify_runtime_debug_event_maps_single_step_finalized_completed(self) -> None: classified = _classify_runtime_debug_event( event_type="worker_log", message="检测步骤跟踪: domain=example.com | step=注册状态检测 | stage=single_step_finalized | elapsed_ms=2700 | ok=1 | detect_key=detect_register | final_status=completed | result_state=passed", payload={"job_code": "sync-overseas-27456", "node_code": "mainland-worker-01"}, ) self.assertEqual("sync-overseas-27456", classified["job_code"]) self.assertTrue(classified["terminal"]) self.assertEqual("completed", classified["terminal_status"]) self.assertEqual("detect_register", classified["step_code"]) def test_build_runtime_display_bucket_prefers_real_active_threads_over_raw_current_load(self) -> None: bucket = _build_runtime_display_bucket( ( "mainland-worker-01", "mainland", "worker", "busy", 753, { "job_items_total": 1000, "job_items_claimed": 0, "job_items_running": 0, "job_items_completed": 342, "active_threads": 61, "max_threads": 400, "detect_participating": True, }, None, ) ) self.assertIsNotNone(bucket) self.assertEqual(61, bucket["current_load"]) self.assertEqual(61, bucket["display_running"]) self.assertEqual(61, bucket["active_threads"]) def test_build_runtime_display_bucket_skips_local_overseas_control_plane_load(self) -> None: with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"): bucket = _build_runtime_display_bucket( ( "overseas-control-01", "overseas", "control", "busy", 371, { "active_threads": 371, "max_threads": 3200, "detect_participating": False, }, None, ) ) self.assertIsNone(bucket) def test_build_runtime_display_bucket_skips_any_control_node_without_worker_support(self) -> None: bucket = _build_runtime_display_bucket( ( "overseas-control-01", "overseas", "control", "busy", 378, { "active_threads": 378, "max_threads": 3200, "detect_participating": False, "worker_online": False, }, None, ) ) self.assertIsNone(bucket) def test_load_latest_runtime_active_job_snapshot_uses_runtime_ingest_when_debug_missing(self) -> None: ingest_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 177, "max_thread_count": 2000, "progress": { "pending": 200, "running": 177, "completed": 440, "blacklisted": 0, "failed": 4, }, "backlog": { "pending_total": 200, "running_total": 177, }, "active_job": { "job_id": 551, "job_code": "sync-overseas-55", "status": "running", "items_total": 821, "items_terminal": 444, "items_failed": 4, "progress_percent": 54.08, "node_stats": [ { "node_code": "mainland-controller-01-a", "items_total": 400, "items_pending": 0, "items_claimed": 80, "items_running": 100, "items_completed": 220, "items_blacklisted": 0, "items_failed": 0, "active_threads": 100, "max_threads": 1000, } ], }, } } fake_conn = _SequenceConn( fetchall_results=[ [], [(ingest_payload, datetime(2026, 4, 22, 18, 5, 36))], ] ) with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value={}): snapshot = _load_latest_runtime_active_job_snapshot(15) self.assertEqual("runtime_ingest", snapshot["_snapshot_source"]) self.assertEqual("sync-overseas-55", snapshot["job"]["job_code"]) self.assertEqual(821, snapshot["queue_health"]["queue"]["items_total"]) self.assertEqual(177, snapshot["queue_health"]["queue"]["display_running"]) self.assertEqual(200, snapshot["backlog"]["pending_total"]) def test_load_latest_runtime_active_job_snapshot_prefers_fresher_runtime_ingest(self) -> None: debug_payload = { "job": { "job_id": 84, "job_code": "sync-overseas-366", "status": "running", }, "queue_health": { "queue": { "items_total": 5000, "running": 2, } }, } ingest_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 177, "max_thread_count": 2000, "progress": { "pending": 200, "running": 177, "completed": 440, "blacklisted": 0, "failed": 4, }, "active_job": { "job_id": 551, "job_code": "sync-overseas-55", "status": "running", "items_total": 821, "items_terminal": 444, "items_failed": 4, }, } } fake_conn = _SequenceConn( fetchall_results=[ [(debug_payload, datetime(2026, 4, 22, 17, 37, 30))], [(ingest_payload, datetime(2026, 4, 22, 18, 5, 36))], ] ) with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value={}): snapshot = _load_latest_runtime_active_job_snapshot(15) self.assertEqual("runtime_ingest", snapshot["_snapshot_source"]) self.assertEqual("sync-overseas-55", snapshot["job"]["job_code"]) def test_load_latest_runtime_active_job_snapshot_prefers_richer_ingest_over_newer_debug_snapshot(self) -> None: debug_payload = { "job": { "job_id": 376, "job_code": "sync-overseas-376", "status": "running", "display_items_running": 138, "display_active_threads": 138, "node_stats": [ { "node_code": "mainland-controller-01", "items_running": 138, "display_running": 138, "active_threads": 138, "max_threads": 2000, } ], }, "queue_health": { "queue": { "items_total": 8402, "running": 138, "display_running": 138, }, "nodes": [ { "node_code": "mainland-controller-01", "items_running": 138, "display_running": 138, "active_threads": 138, "max_threads": 2000, } ], }, } ingest_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 5909, "max_thread_count": 80000, "progress": { "pending": 2493, "running": 5909, "completed": 0, "blacklisted": 0, "failed": 0, }, "active_job": { "job_id": 551, "job_code": "sync-overseas-51", "status": "running", "items_total": 8402, "items_terminal": 0, "items_failed": 0, "display_items_running": 5909, "display_active_threads": 5909, "distributed_node_stats": [ { "node_code": "mainland-controller-01-a", "items_running": 1000, "display_running": 1000, "active_threads": 1000, "max_threads": 1000, }, { "node_code": "mainland-controller-01-b", "items_running": 1000, "display_running": 1000, "active_threads": 1000, "max_threads": 1000, }, { "node_code": "mainland-controller-01-c", "items_running": 3909, "display_running": 3909, "active_threads": 3909, "max_threads": 78000, }, ], }, } } fake_conn = _SequenceConn( fetchall_results=[ [(debug_payload, datetime(2026, 4, 23, 2, 45, 0))], [(ingest_payload, datetime(2026, 4, 23, 2, 38, 55))], ] ) with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value={}): snapshot = _load_latest_runtime_active_job_snapshot(15) self.assertEqual("runtime_ingest", snapshot["_snapshot_source"]) self.assertEqual("sync-overseas-51", snapshot["job"]["job_code"]) self.assertEqual(5909, snapshot["queue_health"]["queue"]["display_running"]) self.assertEqual( [ "mainland-controller-01-a", "mainland-controller-01-b", "mainland-controller-01-c", "unassigned", ], [item["node_code"] for item in snapshot["queue_health"]["nodes"]], ) def test_load_latest_runtime_active_job_snapshot_prefers_focus_job_match_over_newer_debug_snapshot(self) -> None: debug_payload = { "job": { "job_id": 376, "job_code": "sync-overseas-376", "status": "running", }, "queue_health": { "queue": { "items_total": 5000, "running": 282, } }, } ingest_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 177, "max_thread_count": 2000, "progress": { "pending": 7800, "running": 177, "completed": 420, "blacklisted": 0, "failed": 5, }, "active_job": { "job_id": 551, "job_code": "sync-overseas-51", "status": "running", "items_total": 8402, "items_terminal": 425, "items_failed": 5, }, } } fake_conn = _SequenceConn( fetchall_results=[ [(debug_payload, datetime(2026, 4, 22, 23, 17, 47))], [ ({"projection": {"active_job": {"job_code": "sync-overseas-55"}}}, datetime(2026, 4, 22, 18, 6, 10)), (ingest_payload, datetime(2026, 4, 22, 18, 5, 36)), ], ] ) with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch( "app.services.detect_job_service._load_runtime_activity_snapshot", return_value={"focus_job_code": "sync-overseas-51"}, ): snapshot = _load_latest_runtime_active_job_snapshot(15) self.assertEqual("runtime_ingest", snapshot["_snapshot_source"]) self.assertEqual("sync-overseas-51", snapshot["job"]["job_code"]) def test_load_latest_runtime_ingest_active_job_snapshot_keeps_recent_focus_match_over_marginally_newer_non_focus_snapshot(self) -> None: rich_non_focus_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 5909, "max_thread_count": 80000, "progress": { "pending": 2493, "running": 5909, "completed": 0, "blacklisted": 0, "failed": 0, }, "active_job": { "job_id": 198, "job_code": "sync-overseas-198", "status": "running", "items_total": 8402, "items_terminal": 0, "items_failed": 0, "display_items_running": 5909, "display_active_threads": 5909, "distributed_node_stats": [ { "node_code": "mainland-controller-01-a", "items_running": 1000, "display_running": 1000, "active_threads": 1000, "max_threads": 1000, }, { "node_code": "mainland-controller-01-b", "items_running": 4909, "display_running": 4909, "active_threads": 4909, "max_threads": 79000, }, ], }, } } focus_match_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 177, "max_thread_count": 2000, "progress": { "pending": 7800, "running": 177, "completed": 420, "blacklisted": 0, "failed": 5, }, "active_job": { "job_id": 551, "job_code": "sync-overseas-51", "status": "running", "items_total": 8402, "items_terminal": 425, "items_failed": 5, "display_items_running": 177, "display_active_threads": 177, "distributed_node_stats": [ { "node_code": "mainland-controller-01", "items_running": 177, "display_running": 177, "active_threads": 177, "max_threads": 2000, } ], }, } } fake_conn = _SequenceConn( fetchall_results=[ [ (rich_non_focus_payload, datetime(2026, 4, 23, 2, 45, 0)), (focus_match_payload, datetime(2026, 4, 23, 2, 41, 0)), ] ] ) with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): snapshot = detect_job_service_module._load_latest_runtime_ingest_active_job_snapshot( 15, preferred_job_codes=["sync-overseas-51"], ) self.assertEqual("runtime_ingest", snapshot["_snapshot_source"]) self.assertEqual("sync-overseas-51", snapshot["job"]["job_code"]) def test_load_latest_runtime_active_job_snapshot_prefers_newer_richer_ingest_over_stale_focus_job_match(self) -> None: rich_ingest_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 5909, "max_thread_count": 80000, "progress": { "pending": 2493, "running": 5909, "completed": 0, "blacklisted": 0, "failed": 0, }, "active_job": { "job_id": 198, "job_code": "sync-overseas-198", "status": "running", "items_total": 8402, "items_terminal": 0, "items_failed": 0, "display_items_running": 5909, "display_active_threads": 5909, "distributed_node_stats": [ { "node_code": "mainland-controller-01-a", "items_running": 1000, "display_running": 1000, "active_threads": 1000, "max_threads": 1000, }, { "node_code": "mainland-controller-01-b", "items_running": 1000, "display_running": 1000, "active_threads": 1000, "max_threads": 1000, }, { "node_code": "mainland-controller-01-c", "items_running": 3909, "display_running": 3909, "active_threads": 3909, "max_threads": 78000, }, ], }, } } stale_focus_ingest_payload = { "projection": { "node": { "node_code": "mainland-controller-01", "region": "mainland", "role": "control", }, "active_thread_count": 138, "max_thread_count": 2000, "progress": { "pending": 5001, "running": 0, "completed": 0, "blacklisted": 0, "failed": 0, }, "active_job": { "job_id": 51, "job_code": "sync-overseas-51", "status": "running", "items_total": 8402, "items_terminal": 3402, "items_failed": 0, "display_items_running": 138, "display_active_threads": 138, "distributed_node_stats": [ { "node_code": "mainland-controller-01", "items_running": 0, "display_running": 138, "active_threads": 138, "max_threads": 2000, } ], }, } } fake_conn = _SequenceConn( fetchall_results=[ [], [ (rich_ingest_payload, datetime(2026, 4, 23, 2, 38, 55)), (stale_focus_ingest_payload, datetime(2026, 4, 22, 18, 2, 4)), ], ] ) with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch( "app.services.detect_job_service._load_runtime_activity_snapshot", return_value={"focus_job_code": "sync-overseas-51", "job_codes": ["sync-overseas-51"]}, ): snapshot = _load_latest_runtime_active_job_snapshot(15) self.assertEqual("runtime_ingest", snapshot["_snapshot_source"]) self.assertEqual("sync-overseas-198", snapshot["job"]["job_code"]) self.assertEqual(5909, snapshot["queue_health"]["queue"]["display_running"]) def test_get_active_detect_job_summary_falls_back_to_runtime_snapshot(self) -> None: runtime_snapshot = { "job": { "job_id": 551, "job_code": "sync-overseas-55", "status": "running", "progress_percent": 54.08, "node_stats": [ { "node_code": "mainland-controller-01", "items_total": 821, "items_pending": 200, "items_claimed": 0, "items_running": 177, "items_completed": 440, "items_blacklisted": 0, "items_failed": 4, "active_threads": 177, "max_threads": 2000, } ], }, "queue_health": { "queue": { "items_total": 821, "pending": 200, "claimed": 0, "running": 177, "display_claimed": 0, "display_running": 177, "completed": 440, "blacklisted": 0, "failed": 4, "terminal": 444, }, "nodes": [ { "node_code": "mainland-controller-01", "items_total": 821, "items_pending": 200, "items_claimed": 0, "items_running": 177, "items_completed": 440, "items_blacklisted": 0, "items_failed": 4, "active_threads": 177, "max_threads": 2000, } ], "throughput": { "processed_recent": 0, "processed_per_minute": 0, "completed_recent": 0, "blacklisted_recent": 0, "failed_recent": 0, }, }, "_created_at": "2026-04-22 18:05:36", "_snapshot_source": "runtime_ingest", } fake_conn = _SequenceConn(fetchall_results=[[]]) with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value={}): summary = get_active_detect_job_summary(event_limit=10) self.assertIsNotNone(summary) self.assertEqual("sync-overseas-55", summary["job_code"]) self.assertEqual(821, summary["items_total"]) self.assertEqual(177, summary["display_items_running"]) def test_recycle_expired_detect_job_items_once_refreshes_touched_jobs(self) -> None: conn = MagicMock() main_cursor_cm = MagicMock() main_cursor = MagicMock() unlock_cursor_cm = MagicMock() unlock_cursor = MagicMock() main_cursor_cm.__enter__.return_value = main_cursor main_cursor_cm.__exit__.return_value = False unlock_cursor_cm.__enter__.return_value = unlock_cursor unlock_cursor_cm.__exit__.return_value = False conn.cursor.side_effect = [main_cursor_cm, unlock_cursor_cm] main_cursor.fetchone.side_effect = [ (True,), (2, [11, 12]), ("domain_pipeline", 1, 0, 0, 0, 0), ("single_step", 0, 0, 1, 0, 0), ] db_cm = MagicMock() db_cm.__enter__.return_value = conn db_cm.__exit__.return_value = False with patch("app.services.detect_job_service.get_db", return_value=db_cm): with patch("app.services.detect_job_service.push_debug_event") as mock_push: recycled = detect_job_service_module._recycle_expired_detect_job_items_once() self.assertEqual(2, recycled) self.assertTrue(any("UPDATE detect_job_items" in call.args[0] for call in main_cursor.execute.call_args_list)) self.assertTrue(any("FOR UPDATE SKIP LOCKED" in call.args[0] for call in main_cursor.execute.call_args_list)) self.assertTrue( any( "UPDATE detect_jobs" in call.args[0] and call.args[1] == (11,) for call in main_cursor.execute.call_args_list ) ) self.assertTrue( any( "UPDATE detect_jobs" in call.args[0] and call.args[1] == ("failed", 12) for call in main_cursor.execute.call_args_list ) ) unlock_cursor.execute.assert_called_once_with( "SELECT pg_advisory_unlock(%s)", (detect_job_service_module._DETECT_JOB_ITEM_RECYCLE_ADVISORY_LOCK_KEY,), ) self.assertGreaterEqual(conn.commit.call_count, 1) conn.rollback.assert_not_called() mock_push.assert_called_once() def test_recycle_expired_detect_job_items_once_skips_when_lock_is_busy(self) -> None: conn = MagicMock() main_cursor_cm = MagicMock() main_cursor = MagicMock() main_cursor_cm.__enter__.return_value = main_cursor main_cursor_cm.__exit__.return_value = False conn.cursor.return_value = main_cursor_cm main_cursor.fetchone.return_value = (False,) db_cm = MagicMock() db_cm.__enter__.return_value = conn db_cm.__exit__.return_value = False with patch("app.services.detect_job_service.get_db", return_value=db_cm): with patch("app.services.detect_job_service.push_debug_event") as mock_push: recycled = detect_job_service_module._recycle_expired_detect_job_items_once() self.assertEqual(0, recycled) conn.rollback.assert_called_once() conn.commit.assert_not_called() mock_push.assert_not_called() def test_get_active_detect_job_summary_attempts_expired_item_recycle_before_query(self) -> None: fake_conn = _SequenceConn(fetchall_results=[[]]) with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch("app.services.detect_job_service._maybe_recycle_expired_detect_job_items") as mock_recycle: with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value={}): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value={}): summary = get_active_detect_job_summary(event_limit=10) self.assertIsNone(summary) mock_recycle.assert_called_once_with() def test_get_active_detect_job_summary_prefers_runtime_focus_job_code(self) -> None: rows = [ ( 85, "sync-overseas-376", "manual", "domain_pipeline", "", "running", "tester", datetime(2026, 4, 22, 22, 0, 0), datetime(2026, 4, 22, 22, 0, 1), None, ), ( 11, "sync-overseas-51", "manual", "domain_pipeline", "", "running", "tester", datetime(2026, 4, 22, 17, 0, 0), datetime(2026, 4, 22, 17, 0, 1), None, ), ] fake_conn = _SequenceConn(fetchall_results=[rows]) selected_rows = [] def _fake_fetch(_cur, row, event_limit=20): selected_rows.append((row, event_limit)) return {"job_id": row[0], "job_code": row[1], "status": row[5]} with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch( "app.services.detect_job_service._load_runtime_activity_snapshot", return_value={"focus_job_code": "sync-overseas-51", "job_codes": ["sync-overseas-51"]}, ): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value={}): with patch("app.services.detect_job_service._fetch_job_summary", side_effect=_fake_fetch): with patch( "app.services.detect_job_service._enrich_active_job_summary_with_runtime", side_effect=lambda summary, **kwargs: summary, ): summary = get_active_detect_job_summary(event_limit=10) self.assertEqual("sync-overseas-51", summary["job_code"]) self.assertEqual("sync-overseas-51", selected_rows[0][0][1]) self.assertEqual(10, selected_rows[0][1]) def test_get_active_detect_job_summary_prefers_runtime_snapshot_job_code_over_stale_runtime_focus(self) -> None: rows = [ ( 85, "sync-overseas-376", "manual", "domain_pipeline", "", "running", "tester", datetime(2026, 4, 22, 22, 0, 0), datetime(2026, 4, 22, 22, 0, 1), None, ), ( 11, "sync-overseas-51", "manual", "domain_pipeline", "", "running", "tester", datetime(2026, 4, 22, 17, 0, 0), datetime(2026, 4, 22, 17, 0, 1), None, ), ] fake_conn = _SequenceConn(fetchall_results=[rows]) selected_rows = [] def _fake_fetch(_cur, row, event_limit=20): selected_rows.append((row, event_limit)) return {"job_id": row[0], "job_code": row[1], "status": row[5]} with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch( "app.services.detect_job_service._load_runtime_activity_snapshot", return_value={"focus_job_code": "sync-overseas-51", "job_codes": ["sync-overseas-51"]}, ): with patch( "app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value={"job": {"job_code": "sync-overseas-376"}}, ): with patch("app.services.detect_job_service._fetch_job_summary", side_effect=_fake_fetch): with patch( "app.services.detect_job_service._enrich_active_job_summary_with_runtime", side_effect=lambda summary, **kwargs: summary, ): summary = get_active_detect_job_summary(event_limit=10) self.assertEqual("sync-overseas-376", summary["job_code"]) self.assertEqual("sync-overseas-376", selected_rows[0][0][1]) self.assertEqual(10, selected_rows[0][1]) def test_get_active_detect_job_summary_falls_back_when_detect_jobs_legacy_columns_are_missing(self) -> None: rows = [ ( 85, "sync-overseas-376", "manual", "domain_pipeline", "", "running", "tester", datetime(2026, 4, 22, 22, 0, 0), datetime(2026, 4, 22, 22, 0, 1), None, ), ] class LegacyCursor(_SequenceCursor): def execute(self, sql, params=None): self.exec_calls.append((sql, params)) normalized_sql = str(sql or "") if ( "FROM detect_jobs" in normalized_sql and "task_mode" in normalized_sql and " AS task_mode" not in normalized_sql ): raise errors.UndefinedColumn() class LegacyConn(_SequenceConn): def __init__(self) -> None: self.cursor_obj = LegacyCursor(fetchall_results=[rows]) fake_conn = LegacyConn() with patch.object(detect_job_service_module, "_DETECT_JOBS_LEGACY_SELECT_MODE", None): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value={}): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value={}): with patch( "app.services.detect_job_service._fetch_job_summary", side_effect=lambda _cur, row, event_limit=20: { "job_id": row[0], "job_code": row[1], "task_mode": row[3], "step_code": row[4], "status": row[5], }, ): with patch( "app.services.detect_job_service._enrich_active_job_summary_with_runtime", side_effect=lambda summary, **kwargs: summary, ): summary = get_active_detect_job_summary(event_limit=10) self.assertEqual("sync-overseas-376", summary["job_code"]) self.assertEqual("domain_pipeline", summary["task_mode"]) self.assertEqual("", summary["step_code"]) self.assertTrue(any(" AS task_mode" in call[0] for call in fake_conn.cursor_obj.exec_calls)) def test_build_display_summary_ignores_inflated_raw_current_load(self) -> None: summary = _build_display_summary( [ { "node_code": "mainland-controller-01", "items_claimed": 0, "items_running": 0, "display_running": 729, "current_load": 729, "active_threads": 729, "max_threads": 800, "items_completed": 196, "items_failed": 0, }, { "node_code": "mainland-worker-01", "items_claimed": 0, "items_running": 0, "display_running": 753, "current_load": 753, "active_threads": 61, "max_threads": 400, "items_completed": 146, "items_failed": 0, }, ] ) self.assertEqual(790, summary["display_running"]) self.assertEqual(790, summary["current_load"]) self.assertEqual(790, summary["active_threads"]) def test_enrich_active_job_summary_with_runtime_adds_runtime_job_code_and_recent_events(self) -> None: summary = _enrich_active_job_summary_with_runtime( { "job_id": 275, "job_code": "sync-overseas-5297", "processed_recent": 0, "processed_per_minute": 0, "completed_recent": 0, "failed_recent": 0, "blacklisted_recent": 0, "recent_domain_events": [], }, event_limit=20, window_minutes=15, runtime_activity={ "focus_job_code": "sync-overseas-9506", "job_codes": ["sync-overseas-9506", "sync-overseas-27456"], "processed_recent": 120, "completed_recent": 118, "failed_recent": 1, "blacklisted_recent": 1, }, runtime_snapshot={ "job": { "job_id": 1835, "job_code": "sync-overseas-9506", "progress_percent": 44.2, }, "queue_health": { "queue": { "items_total": 1000, "pending": 176, "claimed": 223, "running": 159, "completed": 442, "display_running": 220, } }, }, recent_domain_events=[ { "event_type": "domain_completed", "message": "域名检测完成: example.com", "created_at": "2026-04-21 04:10:00", } ], ) self.assertEqual("sync-overseas-9506", summary["runtime_job_code"]) self.assertEqual(["sync-overseas-9506", "sync-overseas-27456"], summary["runtime_job_codes"]) self.assertEqual(120, summary["processed_recent"]) self.assertEqual(8.0, summary["processed_per_minute"]) self.assertEqual(118, summary["completed_recent"]) self.assertEqual(1, summary["failed_recent"]) self.assertEqual(1, summary["blacklisted_recent"]) self.assertEqual(1, len(summary["recent_domain_events"])) self.assertEqual("sync-overseas-9506", summary["runtime_snapshot_job_code"]) self.assertEqual(1000, summary["runtime_snapshot_queue"]["items_total"]) self.assertEqual(1000, summary["items_total"]) self.assertEqual(176, summary["items_pending"]) self.assertEqual(223, summary["items_claimed"]) self.assertEqual(159, summary["items_running"]) self.assertEqual(442, summary["items_completed"]) self.assertEqual(220, summary["display_items_running"]) def test_enrich_active_job_summary_with_runtime_prefers_snapshot_identity_and_events(self) -> None: summary = _enrich_active_job_summary_with_runtime( { "job_id": 275, "job_code": "sync-overseas-5297", "status": "running", "recent_events": [ { "node_code": "mainland-worker-01", "event_type": "domain_started", "message": "开始检测域名: stale.com", "payload": {"job_code": "sync-overseas-5297"}, "created_at": "2026-04-21 11:58:00", } ], "current_cycle_events": [], "latest_event": None, "display_items_running": 0, "display_current_load": 0, "display_active_threads": 0, "display_max_threads": 0, "display_active_node_codes": [], }, event_limit=10, window_minutes=15, runtime_activity={ "focus_job_code": "sync-overseas-31437", "job_codes": ["sync-overseas-31437"], "processed_recent": 10, "completed_recent": 10, "failed_recent": 0, "blacklisted_recent": 0, }, runtime_snapshot={ "job": { "job_id": 1902, "job_code": "sync-overseas-31437", "status": "running", "progress_percent": 34.3, "node_stats": [ { "node_code": "mainland-controller-01", "items_claimed": 230, "items_running": 67, "items_completed": 343, "items_failed": 0, "active_threads": 170, "max_threads": 2000, }, { "node_code": "mainland-worker-01", "items_claimed": 75, "items_running": 0, "items_completed": 0, "items_failed": 0, "active_threads": 19, "max_threads": 1200, }, ], }, "queue_health": { "queue": { "items_total": 1000, "pending": 285, "claimed": 305, "running": 67, "completed": 343, "display_running": 189, } }, "recent_events": [ { "job_id": 2137, "node_code": "mainland-controller-01", "event_type": "job_created", "message": "同步拉取待检测批次 sync-overseas-35461,共 1000 个任务项", "payload": {"source_record_id": 35461}, "created_at": "2026-04-22 01:21:29", }, { "job_id": 1902, "node_code": "mainland-controller-01", "event_type": "worker_log", "message": "从任务队列获取到 125 个需要检测的域名", "payload": {"job_code": "sync-overseas-31437"}, "created_at": "2026-04-22 01:19:25", }, ], }, ) self.assertEqual(1902, summary["job_id"]) self.assertEqual("sync-overseas-31437", summary["job_code"]) self.assertEqual("sync-overseas-31437", summary["runtime_job_code"]) self.assertEqual(189, summary["display_items_running"]) self.assertEqual(189, summary["display_current_load"]) self.assertEqual(189, summary["display_active_threads"]) self.assertEqual(3200, summary["display_max_threads"]) self.assertEqual(["mainland-controller-01", "mainland-worker-01"], summary["display_active_node_codes"]) self.assertEqual(1, len(summary["recent_events"])) self.assertEqual("worker_log", summary["latest_event"]["event_type"]) self.assertEqual("sync-overseas-31437", summary["recent_events"][0]["payload"]["job_code"]) def test_enrich_active_job_summary_with_runtime_does_not_promote_unassigned_snapshot_running(self) -> None: summary = _enrich_active_job_summary_with_runtime( { "job_id": 384, "job_code": "sync-overseas-16654", "status": "running", "display_items_running": 331, "display_current_load": 331, "display_active_threads": 331, "display_max_threads": 1000, "display_active_node_codes": [], }, event_limit=10, window_minutes=15, runtime_activity={ "focus_job_code": "sync-overseas-16654", "job_codes": ["sync-overseas-16654"], "processed_recent": 0, "completed_recent": 0, "failed_recent": 0, "blacklisted_recent": 0, }, runtime_snapshot={ "_has_runtime_node_rows": True, "_dropped_runtime_node_count": 0, "job": { "job_id": 384, "job_code": "sync-overseas-16654", "status": "running", "node_stats": [ { "node_code": "unassigned", "items_claimed": 23, "items_running": 1863, "display_running": 1863, "active_threads": 0, "max_threads": 0, } ], }, "queue_health": { "queue": { "items_total": 4000, "pending": 0, "claimed": 23, "running": 2100, "completed": 1117, "blacklisted": 0, "failed": 760, "display_running": 2100, }, "nodes": [ { "node_code": "unassigned", "items_claimed": 23, "items_running": 1863, "display_running": 1863, "active_threads": 0, "max_threads": 0, } ], }, }, ) self.assertEqual(0, summary["display_items_running"]) self.assertEqual(0, summary["display_active_threads"]) self.assertEqual([], summary["display_active_node_codes"]) def test_get_detect_queue_health_preserves_runtime_node_throughput_after_snapshot_override(self) -> None: lease_row = (None, None, 0, 0) throughput_rows = [] step_throughput_rows = [] runtime_display_rows = [ ( "mainland-controller-01", "mainland", "control", "busy", 170, { "job_items_total": 265, "job_items_claimed": 85, "job_items_running": 0, "job_items_completed": 105, "active_threads": 170, "max_threads": 2000, "detect_participating": True, }, None, ), ( "mainland-worker-01", "mainland", "worker", "busy", 19, { "job_items_total": 139, "job_items_claimed": 64, "job_items_running": 0, "job_items_completed": 4, "active_threads": 19, "max_threads": 1200, "detect_participating": True, }, None, ), ( "overseas-control-01", "overseas", "control", "busy", 371, { "active_threads": 371, "max_threads": 3200, "detect_participating": False, }, None, ), ] conn = MagicMock() cursor_cm = MagicMock() cursor = MagicMock() conn.cursor.return_value = cursor_cm cursor_cm.__enter__.return_value = cursor db_cm = MagicMock() db_cm.__enter__.return_value = conn cursor.fetchone.return_value = lease_row cursor.fetchall.side_effect = [throughput_rows, step_throughput_rows, runtime_display_rows] active_job = { "job_id": 1937, "job_code": "sync-overseas-31987", "status": "running", "items_total": 1000, "items_pending": 596, "items_claimed": 149, "items_running": 146, "items_completed": 109, "items_blacklisted": 0, "items_failed": 0, "progress_percent": 10.9, "node_stats": [], "distributed_node_stats": [ { "node_code": "mainland-controller-01", "items_total": 265, "items_pending": 0, "items_claimed": 85, "items_running": 75, "items_completed": 105, "items_blacklisted": 0, "items_failed": 0, "metrics_source": "runtime", }, { "node_code": "mainland-worker-01", "items_total": 139, "items_pending": 0, "items_claimed": 64, "items_running": 71, "items_completed": 4, "items_blacklisted": 0, "items_failed": 0, "metrics_source": "runtime", }, ], "step_stats": [ { "step_code": "detect_register", "items_total": 1000, "items_pending": 449, "items_claimed": 131, "items_running": 273, "items_completed": 147, "items_blacklisted": 0, "items_failed": 0, } ], } runtime_activity = { "processed_recent": 133, "completed_recent": 113, "failed_recent": 20, "blacklisted_recent": 0, "focus_job_code": "sync-overseas-31563", "job_codes": ["sync-overseas-31563"], "step_code": "detect_register", "nodes": { "mainland-controller-01": { "node_code": "mainland-controller-01", "processed_recent": 74, "completed_recent": 54, "failed_recent": 20, "blacklisted_recent": 0, }, "mainland-worker-01": { "node_code": "mainland-worker-01", "processed_recent": 59, "completed_recent": 59, "failed_recent": 0, "blacklisted_recent": 0, }, }, } runtime_snapshot = { "job": { "job_id": 1937, "job_code": "sync-overseas-31987", "progress_percent": 10.9, }, "queue_health": { "queue": { "items_total": 1000, "pending": 596, "claimed": 149, "running": 146, "completed": 109, "blacklisted": 0, "failed": 0, "display_claimed": 149, "display_running": 403, }, "nodes": [ { "node_code": "mainland-controller-01", "items_total": 265, "items_pending": 0, "items_claimed": 85, "items_running": 75, "items_completed": 105, "items_blacklisted": 0, "items_failed": 0, "processed_recent": 0, "processed_per_minute": 0, "completed_recent": 0, "blacklisted_recent": 0, "failed_recent": 0, "metrics_source": "runtime", }, { "node_code": "mainland-worker-01", "items_total": 139, "items_pending": 0, "items_claimed": 64, "items_running": 71, "items_completed": 4, "items_blacklisted": 0, "items_failed": 0, "processed_recent": 0, "processed_per_minute": 0, "completed_recent": 0, "blacklisted_recent": 0, "failed_recent": 0, "metrics_source": "runtime", }, ], "steps": [], }, } with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"): with patch("app.services.detect_job_service.get_active_detect_job_summary", return_value=active_job): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value=runtime_activity): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot): with patch("app.services.detect_job_service._load_runtime_display_rows", return_value=runtime_display_rows): with patch("app.services.detect_job_service.get_db", return_value=db_cm): health = get_detect_queue_health(window_minutes=15) node_map = {item["node_code"]: item for item in health["nodes"]} self.assertEqual(74, node_map["mainland-controller-01"]["processed_recent"]) self.assertEqual(54, node_map["mainland-controller-01"]["completed_recent"]) self.assertEqual(20, node_map["mainland-controller-01"]["failed_recent"]) self.assertEqual(170, node_map["mainland-controller-01"]["items_running"]) self.assertEqual(59, node_map["mainland-worker-01"]["processed_recent"]) self.assertEqual(59, node_map["mainland-worker-01"]["completed_recent"]) self.assertEqual(19, node_map["mainland-worker-01"]["items_running"]) self.assertEqual(113, health["throughput"]["completed_recent"]) self.assertEqual(189, health["queue"]["display_running"]) self.assertNotIn("overseas-control-01", node_map) def test_get_detect_queue_health_preserves_snapshot_node_display_metrics_for_child_instances(self) -> None: lease_row = (None, None, 0, 0) throughput_rows = [] step_throughput_rows = [] runtime_display_rows = [] conn = MagicMock() cursor_cm = MagicMock() cursor = MagicMock() conn.cursor.return_value = cursor_cm cursor_cm.__enter__.return_value = cursor db_cm = MagicMock() db_cm.__enter__.return_value = conn cursor.fetchone.return_value = lease_row cursor.fetchall.side_effect = [throughput_rows, step_throughput_rows, runtime_display_rows] active_job = { "job_id": 198, "job_code": "sync-overseas-198", "status": "running", "items_total": 10344, "items_pending": 4945, "items_claimed": 0, "items_running": 5909, "items_completed": 5344, "items_blacklisted": 0, "items_failed": 42, "progress_percent": 52.07, "display_items_claimed": 13, "display_items_running": 11799, "display_active_threads": 11799, "display_max_threads": 75400, "node_stats": [], "distributed_node_stats": [ { "node_code": "mainland-controller-01-ae", "items_total": 947, "items_pending": 0, "items_claimed": 0, "items_running": 947, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, }, { "node_code": "mainland-controller-01-au", "items_total": 637, "items_pending": 0, "items_claimed": 0, "items_running": 637, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, }, ], "step_stats": [], } runtime_activity = { "processed_recent": 0, "completed_recent": 0, "failed_recent": 0, "blacklisted_recent": 0, "focus_job_code": "sync-overseas-198", "job_codes": ["sync-overseas-198"], "step_code": "detect_register", "nodes": {}, } runtime_snapshot = { "job": { "job_id": 198, "job_code": "sync-overseas-198", "progress_percent": 52.07, }, "queue_health": { "queue": { "items_total": 10344, "pending": 4945, "claimed": 0, "running": 5909, "completed": 5344, "blacklisted": 0, "failed": 42, "display_claimed": 13, "display_running": 5909, }, "nodes": [ { "node_code": "mainland-controller-01-ae", "items_total": 947, "items_pending": 0, "items_claimed": 0, "items_running": 947, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "display_running": 947, "current_load": 947, "active_threads": 947, "max_threads": 1000, }, { "node_code": "mainland-controller-01-au", "items_total": 637, "items_pending": 0, "items_claimed": 0, "items_running": 637, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "display_running": 637, "current_load": 637, "active_threads": 637, "max_threads": 1000, }, ], "steps": [], }, } with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"): with patch("app.services.detect_job_service.get_active_detect_job_summary", return_value=active_job): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value=runtime_activity): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot): with patch("app.services.detect_job_service.get_db", return_value=db_cm): with patch("app.services.detect_job_service._load_disabled_managed_node_codes", return_value=set()): health = get_detect_queue_health(window_minutes=15) node_map = {item["node_code"]: item for item in health["nodes"]} self.assertEqual(1584, health["queue"]["display_running"]) self.assertEqual(947, node_map["mainland-controller-01-ae"]["display_running"]) self.assertEqual(947, node_map["mainland-controller-01-ae"]["active_threads"]) self.assertEqual(1000, node_map["mainland-controller-01-ae"]["max_threads"]) self.assertEqual(637, node_map["mainland-controller-01-au"]["display_running"]) self.assertEqual(637, node_map["mainland-controller-01-au"]["active_threads"]) def test_get_detect_queue_health_drops_stale_runtime_nodes_from_display_running(self) -> None: lease_row = (None, None, 0, 0) throughput_rows = [] step_throughput_rows = [] runtime_display_rows = [] conn = MagicMock() cursor_cm = MagicMock() cursor = MagicMock() conn.cursor.return_value = cursor_cm cursor_cm.__enter__.return_value = cursor db_cm = MagicMock() db_cm.__enter__.return_value = conn cursor.fetchone.return_value = lease_row cursor.fetchall.side_effect = [throughput_rows, step_throughput_rows, runtime_display_rows] stale_heartbeat = (datetime.now() - timedelta(minutes=8)).isoformat(sep=" ", timespec="seconds") live_heartbeat = datetime.now().isoformat(sep=" ", timespec="seconds") active_job = { "job_id": 199, "job_code": "sync-overseas-199", "status": "running", "items_total": 2600, "items_pending": 500, "items_claimed": 0, "items_running": 2100, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "progress_percent": 80.77, "display_items_running": 2100, "display_active_threads": 2100, "display_max_threads": 2000, "node_stats": [], "distributed_node_stats": [ { "node_code": "mainland-controller-01-a", "items_total": 300, "items_pending": 0, "items_claimed": 0, "items_running": 300, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, }, { "node_code": "mainland-controller-01-b", "items_total": 1800, "items_pending": 0, "items_claimed": 0, "items_running": 1800, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, }, ], "step_stats": [], } runtime_activity = { "processed_recent": 0, "completed_recent": 0, "failed_recent": 0, "blacklisted_recent": 0, "focus_job_code": "sync-overseas-199", "job_codes": ["sync-overseas-199"], "step_code": "detect_register", "nodes": {}, } runtime_snapshot = { "_created_at": datetime.now().isoformat(sep=" ", timespec="seconds"), "job": { "job_id": 199, "job_code": "sync-overseas-199", "progress_percent": 80.77, }, "queue_health": { "queue": { "items_total": 2600, "pending": 500, "claimed": 0, "running": 2100, "completed": 0, "blacklisted": 0, "failed": 0, "display_claimed": 0, "display_running": 2100, }, "nodes": [ { "node_code": "mainland-controller-01-a", "items_total": 300, "items_pending": 0, "items_claimed": 0, "items_running": 300, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "display_running": 300, "current_load": 300, "active_threads": 300, "max_threads": 1000, "status": "busy", "last_heartbeat_at": live_heartbeat, }, { "node_code": "mainland-controller-01-b", "items_total": 1800, "items_pending": 0, "items_claimed": 0, "items_running": 1800, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "display_running": 1800, "current_load": 1800, "active_threads": 1800, "max_threads": 1000, "status": "stale", "last_heartbeat_at": stale_heartbeat, }, ], "steps": [], }, } with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"): with patch("app.services.detect_job_service.get_active_detect_job_summary", return_value=active_job): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value=runtime_activity): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot): with patch("app.services.detect_job_service._load_runtime_display_rows", return_value=runtime_display_rows): with patch("app.services.detect_job_service.get_db", return_value=db_cm): health = get_detect_queue_health(window_minutes=15) self.assertEqual(300, health["queue"]["running"]) self.assertEqual(300, health["queue"]["display_running"]) self.assertEqual(["mainland-controller-01-a"], [item["node_code"] for item in health["nodes"]]) def test_enrich_active_job_summary_drops_old_runtime_ingest_nodes_without_heartbeat(self) -> None: stale_snapshot_created_at = (datetime.now() - timedelta(minutes=12)).isoformat(sep=" ", timespec="seconds") summary = _enrich_active_job_summary_with_runtime( { "job_id": 384, "job_code": "sync-overseas-16654", "status": "running", "display_items_running": 0, "display_current_load": 0, "display_active_threads": 0, "display_max_threads": 0, "display_active_node_codes": [], }, event_limit=10, window_minutes=15, runtime_activity={ "focus_job_code": "sync-overseas-16654", "job_codes": ["sync-overseas-16654"], "processed_recent": 0, "completed_recent": 0, "failed_recent": 0, "blacklisted_recent": 0, }, runtime_snapshot={ "_snapshot_source": "runtime_ingest", "_created_at": stale_snapshot_created_at, "job": { "job_id": 384, "job_code": "sync-overseas-16654", "status": "running", "display_items_running": 2100, "display_active_threads": 2100, "node_stats": [ { "node_code": "mainland-controller-01-ah", "items_running": 62, "display_running": 62, }, { "node_code": "mainland-controller-01-bn", "items_running": 61, "display_running": 61, }, { "node_code": "unassigned", "items_running": 1863, "display_running": 1863, }, ], }, "queue_health": { "queue": { "items_total": 4000, "pending": 0, "claimed": 23, "running": 2100, "completed": 1117, "blacklisted": 0, "failed": 760, "display_running": 2100, }, "nodes": [ { "node_code": "mainland-controller-01-ah", "items_running": 62, "display_running": 62, }, { "node_code": "mainland-controller-01-bn", "items_running": 61, "display_running": 61, }, { "node_code": "unassigned", "items_running": 1863, "display_running": 1863, }, ], }, }, ) self.assertEqual(0, summary["display_items_running"]) self.assertEqual(0, summary["display_active_threads"]) self.assertEqual([], summary["display_active_node_codes"]) def test_get_detect_queue_health_aligns_runtime_job_identity_with_active_job(self) -> None: lease_row = (None, None, 0, 0) throughput_rows = [] step_throughput_rows = [] runtime_display_rows = [] conn = MagicMock() cursor_cm = MagicMock() cursor = MagicMock() conn.cursor.return_value = cursor_cm cursor_cm.__enter__.return_value = cursor db_cm = MagicMock() db_cm.__enter__.return_value = conn cursor.fetchone.return_value = lease_row cursor.fetchall.side_effect = [throughput_rows, step_throughput_rows, runtime_display_rows] active_job = { "job_id": 112, "job_code": "sync-overseas-599", "runtime_job_code": "sync-overseas-599", "runtime_snapshot_job_code": "sync-overseas-599", "status": "running", "items_total": 5274, "items_pending": 4959, "items_claimed": 0, "items_running": 840, "items_completed": 274, "items_blacklisted": 0, "items_failed": 21, "progress_percent": 5.59, "display_items_claimed": 17, "display_items_running": 1377, "display_active_threads": 1377, "display_max_threads": 81200, "node_stats": [], "distributed_node_stats": [], "step_stats": [], } runtime_activity = { "processed_recent": 93, "completed_recent": 24, "failed_recent": 69, "blacklisted_recent": 0, "focus_job_code": "sync-overseas-198", "job_codes": ["sync-overseas-198", "sync-overseas-97"], "step_code": "detect_360_site", "step_stats": {}, "nodes": {}, } runtime_snapshot = { "job": { "job_id": 112, "job_code": "sync-overseas-599", "status": "running", "progress_percent": 5.59, }, "queue_health": { "queue": { "items_total": 5274, "pending": 4959, "claimed": 0, "running": 1377, "display_claimed": 17, "display_running": 1377, "completed": 274, "blacklisted": 0, "failed": 21, }, "nodes": [], "steps": [], }, } with patch("app.services.detect_job_service.get_active_detect_job_summary", return_value=active_job): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value=runtime_activity): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot): with patch("app.services.detect_job_service.get_db", return_value=db_cm): with patch("app.services.detect_job_service._load_disabled_managed_node_codes", return_value=set()): health = get_detect_queue_health(window_minutes=15) self.assertEqual("sync-overseas-599", health["job"]["job_code"]) self.assertEqual("sync-overseas-599", health["job"]["runtime_job_code"]) self.assertEqual("sync-overseas-599", health["job"]["runtime_job_codes"][0]) self.assertEqual(1377, health["queue"]["running"]) self.assertEqual(1377, health["queue"]["display_running"]) def test_get_detect_queue_health_ignores_mismatched_runtime_overlay_for_unrelated_job(self) -> None: lease_row = (None, None, 0, 0) throughput_rows = [] step_throughput_rows = [] runtime_display_rows = [] conn = MagicMock() cursor_cm = MagicMock() cursor = MagicMock() conn.cursor.return_value = cursor_cm cursor_cm.__enter__.return_value = cursor db_cm = MagicMock() db_cm.__enter__.return_value = conn cursor.fetchone.return_value = lease_row cursor.fetchall.side_effect = [throughput_rows, step_throughput_rows, runtime_display_rows] active_job = { "job_id": 1, "job_code": "detect-20260421232649-acfa3d", "runtime_job_code": "sync-overseas-40", "status": "running", "items_total": 14961, "items_pending": 9478, "items_claimed": 0, "items_running": 1, "items_completed": 4996, "items_blacklisted": 0, "items_failed": 486, "progress_percent": 36.64, "display_items_claimed": 0, "display_items_running": 0, "display_active_threads": 0, "display_max_threads": 0, "node_stats": [], "distributed_node_stats": [ { "node_code": "mainland-worker-01", "items_total": 0, "items_pending": 0, "items_claimed": 0, "items_running": 0, "display_running": 1, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "metrics_source": "agent-heartbeat", "region": "mainland", "role": "worker", "status": "busy", "current_load": 1, "active_threads": 1, "max_threads": 1, "last_heartbeat_at": datetime.now().isoformat(sep=" ", timespec="seconds"), }, { "node_code": "mainland-controller-01", "items_total": 1, "items_pending": 0, "items_claimed": 0, "items_running": 1, "display_running": 0, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "metrics_source": "", "region": "", "role": "", "status": "", "current_load": 1, "active_threads": 0, "max_threads": 0, "last_heartbeat_at": "", }, { "node_code": "unassigned", "items_total": 14960, "items_pending": 14960, "items_claimed": 0, "items_running": 0, "display_running": 0, "items_completed": 0, "items_blacklisted": 0, "items_failed": 0, "metrics_source": "central_queue", }, ], "step_stats": [], } runtime_activity = { "processed_recent": 52, "completed_recent": 0, "failed_recent": 0, "blacklisted_recent": 0, "focus_job_code": "sync-overseas-40", "job_codes": ["sync-overseas-40", "sync-overseas-14"], "step_code": "detect_register", "nodes": { "mainland-controller-01-k": { "node_code": "mainland-controller-01-k", "processed_recent": 20, "completed_recent": 20, "failed_recent": 0, "blacklisted_recent": 0, }, "mainland-controller-01-a": { "node_code": "mainland-controller-01-a", "processed_recent": 16, "completed_recent": 16, "failed_recent": 0, "blacklisted_recent": 0, }, "mainland-controller-01-s": { "node_code": "mainland-controller-01-s", "processed_recent": 16, "completed_recent": 16, "failed_recent": 0, "blacklisted_recent": 0, }, }, } runtime_snapshot = { "_created_at": datetime.now().isoformat(sep=" ", timespec="seconds"), "job": { "job_id": 551, "job_code": "sync-overseas-17149", "status": "running", "progress_percent": 41.2, }, "queue_health": { "queue": { "items_total": 4000, "pending": 0, "claimed": 2352, "running": 51, "display_claimed": 2352, "display_running": 51, "completed": 0, "blacklisted": 0, "failed": 0, }, "nodes": [ { "node_code": "mainland-controller-01-k", "items_total": 1200, "items_pending": 0, "items_claimed": 1179, "items_running": 20, "display_running": 20, "current_load": 20, "active_threads": 20, "max_threads": 900, "status": "busy", "last_heartbeat_at": datetime.now().isoformat(sep=" ", timespec="seconds"), }, { "node_code": "mainland-controller-01-a", "items_total": 1400, "items_pending": 0, "items_claimed": 1383, "items_running": 17, "display_running": 17, "current_load": 17, "active_threads": 17, "max_threads": 900, "status": "busy", "last_heartbeat_at": datetime.now().isoformat(sep=" ", timespec="seconds"), }, { "node_code": "mainland-controller-01-s", "items_total": 1400, "items_pending": 0, "items_claimed": 1395, "items_running": 14, "display_running": 14, "current_load": 14, "active_threads": 14, "max_threads": 900, "status": "busy", "last_heartbeat_at": datetime.now().isoformat(sep=" ", timespec="seconds"), }, ], "steps": [], }, } with patch("app.services.detect_job_service.settings.node_region", "overseas"): with patch("app.services.detect_job_service.settings.node_role", "control"): with patch("app.services.detect_job_service.settings.node_code", "overseas-control-01"): with patch("app.services.detect_job_service.get_active_detect_job_summary", return_value=active_job): with patch("app.services.detect_job_service._load_runtime_activity_snapshot", return_value=runtime_activity): with patch("app.services.detect_job_service._load_latest_runtime_active_job_snapshot", return_value=runtime_snapshot): with patch("app.services.detect_job_service._load_runtime_display_rows", return_value=runtime_display_rows): with patch("app.services.detect_job_service.get_db", return_value=db_cm): health = get_detect_queue_health(window_minutes=15) node_codes = [item["node_code"] for item in health["nodes"]] self.assertEqual("detect-20260421232649-acfa3d", health["job"]["job_code"]) self.assertEqual(2, health["queue"]["running"]) self.assertEqual(2, health["queue"]["display_running"]) self.assertCountEqual(["mainland-worker-01", "mainland-controller-01", "unassigned"], node_codes) self.assertNotIn("mainland-controller-01-k", node_codes) def test_load_runtime_activity_snapshot_prefers_recent_runtime_snapshot_job_code(self) -> None: event_rows = [ ( "mainland-worker-01-a", "domain_completed", "", {"job_code": "sync-overseas-198", "detect_key": "detect_register"}, datetime(2026, 4, 23, 22, 8, 0), ), ( "mainland-controller-01-a", "domain_started", "", {"job_code": "sync-overseas-599", "detect_key": "detect_wayback"}, datetime(2026, 4, 23, 22, 12, 0), ), ( "mainland-controller-01-a", "domain_failed", "", {"job_code": "sync-overseas-599", "detect_key": "detect_wayback"}, datetime(2026, 4, 23, 22, 12, 5), ), ] fake_conn = _SequenceConn(fetchall_results=[event_rows]) with patch.object(detect_job_service_module.settings, "node_region", "overseas"): with patch.object(detect_job_service_module.settings, "node_role", "control"): with patch("app.services.detect_job_service.get_db", return_value=fake_conn): with patch( "app.services.detect_job_service._load_recent_runtime_snapshot_job_codes", return_value=["sync-overseas-599"], ): snapshot = detect_job_service_module._load_runtime_activity_snapshot(15) self.assertEqual("sync-overseas-599", snapshot["focus_job_code"]) self.assertEqual(["sync-overseas-599", "sync-overseas-198"], snapshot["job_codes"]) self.assertEqual(1, snapshot["processed_recent"]) self.assertEqual(1, snapshot["failed_recent"]) self.assertEqual(0, snapshot["completed_recent"]) self.assertEqual("detect_wayback", snapshot["step_code"]) self.assertIn("mainland-controller-01-a", snapshot["nodes"]) self.assertNotIn("mainland-worker-01-a", snapshot["nodes"]) def test_normalize_detect_step_code_accepts_supported_single_step(self) -> None: self.assertEqual("detect_baidu_site", normalize_detect_step_code("detect_baidu_site")) self.assertEqual("detect_wayback", normalize_detect_step_code("detect_wayback")) def test_normalize_detect_step_code_rejects_unknown_step(self) -> None: self.assertEqual("", normalize_detect_step_code("detect_unknown")) def test_resolve_detect_job_definition_builds_single_step_job(self) -> None: definition = resolve_detect_job_definition("detect_baidu_site") self.assertTrue(definition["is_single_step"]) self.assertEqual("single_step", definition["task_mode"]) self.assertEqual("detect_baidu_site", definition["step_code"]) def test_resolve_detect_job_definition_builds_wayback_single_step_job(self) -> None: definition = resolve_detect_job_definition("detect_wayback") self.assertTrue(definition["is_single_step"]) self.assertEqual("single_step", definition["task_mode"]) self.assertEqual("detect_wayback", definition["step_code"]) def test_build_step_payload_adds_wayback_recent_years_strategy(self) -> None: payload = _build_step_payload( step_code="detect_wayback", domain_snapshot={"domain": "example.com", "source_type": 2}, settings_payload={"detect_options": {"detect_wayback": True}}, ) self.assertEqual("detect_wayback", payload["step_code"]) self.assertEqual("recent_years", payload["wayback_strategy"]) self.assertEqual(5, payload["wayback_recent_years"]) self.assertTrue(payload["wayback_stop_on_first_hit"]) def test_resolve_detect_job_definition_defaults_to_domain_pipeline(self) -> None: definition = resolve_detect_job_definition(None) self.assertFalse(definition["is_single_step"]) self.assertEqual("domain_pipeline", definition["task_mode"]) self.assertEqual("", definition["step_code"]) def test_resolve_domain_pipeline_step_skips_yikoujia_register(self) -> None: step_code = resolve_domain_pipeline_step( { "source_type": 1, "register_status": 0, "baidu_site": {}, "qihu360_site": {}, "chinaz_info": {}, "aizhan_info": {}, "wayback_info": {}, "jucha_info": {}, "juziseo_info": {}, }, settings_payload={ "detect_options": { "detect_register": True, "detect_baidu_site": True, "detect_360_site": False, "detect_chinaz": False, "detect_aizhan": False, "detect_wayback": False, "detect_jucha": False, "detect_juziseo": False, "detect_order": ["detect_register", "detect_baidu_site"], } }, ) self.assertEqual("detect_baidu_site", step_code) def test_resolve_domain_pipeline_step_moves_to_next_incomplete_step(self) -> None: step_code = resolve_domain_pipeline_step( { "source_type": 2, "register_status": 3, "baidu_site": {"state": "passed"}, "qihu360_site": {}, "chinaz_info": {}, "aizhan_info": {}, "wayback_info": {}, "jucha_info": {}, "juziseo_info": {}, }, settings_payload={ "detect_options": { "detect_register": True, "detect_baidu_site": True, "detect_360_site": True, "detect_chinaz": False, "detect_aizhan": False, "detect_wayback": False, "detect_jucha": False, "detect_juziseo": False, "detect_order": ["detect_register", "detect_baidu_site", "detect_360_site"], } }, after_step_code="detect_baidu_site", ) self.assertEqual("detect_360_site", step_code) def test_resolve_initial_domain_pipeline_item_builds_first_step_payload(self) -> None: step_code, payload = resolve_initial_domain_pipeline_item( { "id": 10, "domain": "example.com", "source_type": 2, "register_status": 0, "baidu_site": {}, "qihu360_site": {}, "chinaz_info": {}, "aizhan_info": {}, "wayback_info": {}, "jucha_info": {}, "juziseo_info": {}, }, settings_payload={ "detect_options": { "detect_register": True, "detect_baidu_site": True, "detect_order": ["detect_register", "detect_baidu_site"], } }, ) self.assertEqual("detect_register", step_code) self.assertIsNotNone(payload) self.assertEqual("detect_register", payload["step_code"]) self.assertEqual("example.com", payload["domain"]) def test_resolve_initial_domain_pipeline_item_returns_empty_when_pipeline_already_done(self) -> None: step_code, payload = resolve_initial_domain_pipeline_item( { "id": 11, "domain": "done.com", "source_type": 2, "register_status": 3, "baidu_site": {"state": "passed"}, "qihu360_site": {}, "chinaz_info": {}, "aizhan_info": {}, "wayback_info": {}, "jucha_info": {}, "juziseo_info": {}, }, settings_payload={ "detect_options": { "detect_register": True, "detect_baidu_site": True, "detect_order": ["detect_register", "detect_baidu_site"], } }, ) self.assertEqual("", step_code) self.assertIsNone(payload) def test_classify_pipeline_item_outcome_retries_external_failure(self) -> None: outcome = _classify_pipeline_item_outcome( item_status="failed", result_payload={"state": "degraded", "message": "timeout", "retry_recommended": True}, step_code="detect_baidu_site", attempt_count=0, ) self.assertEqual("retry", outcome["action"]) self.assertTrue(outcome["should_retry"]) self.assertEqual("external_retry", outcome["reason_code"]) def test_classify_pipeline_item_outcome_rejects_business_failure(self) -> None: outcome = _classify_pipeline_item_outcome( item_status="failed", result_payload={"state": "rejected", "message": "title contains forbidden keyword"}, step_code="detect_chinaz", attempt_count=0, ) self.assertEqual("reject", outcome["action"]) self.assertFalse(outcome["should_retry"]) self.assertEqual("business_reject", outcome["reason_code"]) def test_classify_pipeline_item_outcome_marks_blacklisted_terminal(self) -> None: outcome = _classify_pipeline_item_outcome( item_status="blacklisted", result_payload={"state": "blacklisted", "message": "risk hit"}, step_code="detect_baidu_site", attempt_count=0, ) self.assertEqual("black_hit", outcome["action"]) self.assertFalse(outcome["should_retry"]) self.assertEqual("blacklisted", outcome["reason_code"]) if __name__ == "__main__": unittest.main()