306 lines
13 KiB
Python
306 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
from app.api.routes import detect as detect_route
|
|
|
|
|
|
class DetectApiRoutesTestCase(unittest.TestCase):
|
|
@patch("app.api.routes.detect.create_detect_run_snapshot")
|
|
@patch("app.api.routes.detect._dispatch_remote_detect_start")
|
|
@patch("app.api.routes.detect.get_settings_payload")
|
|
@patch("app.api.routes.detect.get_detect_status")
|
|
@patch("app.api.routes.detect.send_worker_command")
|
|
@patch("app.api.routes.detect.start_worker")
|
|
@patch("app.api.routes.detect.append_detect_job_event")
|
|
@patch("app.api.routes.detect.create_detect_job_if_needed")
|
|
def test_start_detect_skips_redundant_systemctl_when_worker_already_online(
|
|
self,
|
|
mock_create_job,
|
|
mock_append_event,
|
|
mock_start_worker,
|
|
mock_send_worker_command,
|
|
mock_get_detect_status,
|
|
mock_get_settings_payload,
|
|
mock_dispatch_remote,
|
|
mock_create_snapshot,
|
|
) -> None:
|
|
mock_create_job.return_value = {
|
|
"job_id": 30,
|
|
"job_code": "sync-overseas-30",
|
|
"status": "running",
|
|
"items_pending": 100,
|
|
"items_claimed": 0,
|
|
"items_running": 0,
|
|
"task_mode": "domain_pipeline",
|
|
"step_code": "",
|
|
}
|
|
mock_get_detect_status.return_value = {
|
|
"worker_mode": "linux-systemd",
|
|
"worker_online": True,
|
|
"worker_process_count": 60,
|
|
"worker_latest_start_time": "2026-04-24 23:30:00",
|
|
"worker_runtime_message": "running",
|
|
"progress": {},
|
|
}
|
|
mock_send_worker_command.return_value = (True, "已发送 Worker 控制指令: start_detection")
|
|
mock_get_settings_payload.return_value = {
|
|
"runtime": {"thread_count": 1000},
|
|
"proxy_config": {"proxy_enable": True, "allow_direct": True, "proxy_urls": ["a"]},
|
|
}
|
|
mock_dispatch_remote.return_value = {
|
|
"queued_jobs": [],
|
|
"queued_total": 0,
|
|
"failed_total": 0,
|
|
"target_summary": {"controller_nodes": [], "worker_nodes": []},
|
|
}
|
|
|
|
with patch.object(detect_route.settings, "node_region", "mainland"), patch.object(
|
|
detect_route.settings, "node_role", "control"
|
|
):
|
|
response = detect_route.start_detect()
|
|
|
|
self.assertEqual(0, response.code)
|
|
self.assertIn("检测端已在运行,跳过重复启动", response.message)
|
|
mock_start_worker.assert_not_called()
|
|
mock_send_worker_command.assert_called_once()
|
|
event_types = [call.kwargs.get("event_type") for call in mock_append_event.call_args_list]
|
|
self.assertIn("job_dispatch_start_skipped", event_types)
|
|
self.assertIn("job_dispatch_sent", event_types)
|
|
mock_create_snapshot.assert_called_once()
|
|
|
|
@patch("app.api.routes.detect.create_detect_run_snapshot")
|
|
@patch("app.api.routes.detect._dispatch_remote_detect_start")
|
|
@patch("app.api.routes.detect.get_settings_payload")
|
|
@patch("app.api.routes.detect.get_detect_status")
|
|
@patch("app.api.routes.detect.send_worker_command")
|
|
@patch("app.api.routes.detect.start_worker")
|
|
@patch("app.api.routes.detect.append_detect_job_event")
|
|
@patch("app.api.routes.detect.create_detect_job_if_needed")
|
|
def test_start_detect_falls_back_to_direct_command_when_worker_already_running(
|
|
self,
|
|
mock_create_job,
|
|
mock_append_event,
|
|
mock_start_worker,
|
|
mock_send_worker_command,
|
|
mock_get_detect_status,
|
|
mock_get_settings_payload,
|
|
mock_dispatch_remote,
|
|
mock_create_snapshot,
|
|
) -> None:
|
|
mock_create_job.return_value = {
|
|
"job_id": 31,
|
|
"job_code": "sync-overseas-31",
|
|
"status": "running",
|
|
"items_pending": 100,
|
|
"items_claimed": 0,
|
|
"items_running": 0,
|
|
"task_mode": "domain_pipeline",
|
|
"step_code": "",
|
|
}
|
|
mock_start_worker.return_value = (
|
|
False,
|
|
"domaincheck-worker 控制失败:当前运行用户没有免密 systemctl 权限,请为 API 进程授予对应 sudo/systemd 权限",
|
|
)
|
|
mock_send_worker_command.return_value = (True, "已发送 Worker 控制指令: start_detection")
|
|
mock_get_detect_status.side_effect = [
|
|
{
|
|
"worker_mode": "linux-systemd",
|
|
"worker_online": False,
|
|
"worker_process_count": 0,
|
|
"worker_latest_start_time": "",
|
|
"worker_runtime_message": "starting",
|
|
"progress": {},
|
|
},
|
|
{
|
|
"worker_mode": "linux-systemd",
|
|
"worker_online": True,
|
|
"worker_process_count": 60,
|
|
"worker_latest_start_time": "2026-04-24 23:30:00",
|
|
"worker_runtime_message": "running",
|
|
"progress": {},
|
|
},
|
|
{
|
|
"worker_mode": "linux-systemd",
|
|
"worker_online": True,
|
|
"worker_process_count": 60,
|
|
"worker_latest_start_time": "2026-04-24 23:30:00",
|
|
"worker_runtime_message": "running",
|
|
"progress": {},
|
|
},
|
|
]
|
|
mock_get_settings_payload.return_value = {
|
|
"runtime": {"thread_count": 1000},
|
|
"proxy_config": {"proxy_enable": True, "allow_direct": True, "proxy_urls": ["a"]},
|
|
}
|
|
mock_dispatch_remote.return_value = {
|
|
"queued_jobs": [],
|
|
"queued_total": 0,
|
|
"failed_total": 0,
|
|
"target_summary": {"controller_nodes": [], "worker_nodes": []},
|
|
}
|
|
|
|
with patch.object(detect_route.settings, "node_region", "mainland"), patch.object(
|
|
detect_route.settings, "node_role", "control"
|
|
):
|
|
response = detect_route.start_detect()
|
|
|
|
self.assertEqual(0, response.code)
|
|
self.assertIn("检测端已在运行,改为直接发送控制指令", response.message)
|
|
mock_send_worker_command.assert_called_once()
|
|
event_types = [call.kwargs.get("event_type") for call in mock_append_event.call_args_list]
|
|
self.assertIn("job_dispatch_start_degraded", event_types)
|
|
self.assertIn("job_dispatch_sent", event_types)
|
|
mock_create_snapshot.assert_called_once()
|
|
|
|
@patch("app.api.routes.detect.create_detect_run_snapshot")
|
|
@patch("app.api.routes.detect._dispatch_remote_detect_start")
|
|
@patch("app.api.routes.detect.get_settings_payload")
|
|
@patch("app.api.routes.detect.get_detect_status")
|
|
@patch("app.api.routes.detect.send_worker_command")
|
|
@patch("app.api.routes.detect.start_worker")
|
|
@patch("app.api.routes.detect.append_detect_job_event")
|
|
@patch("app.api.routes.detect.create_detect_job_if_needed")
|
|
def test_start_detect_skips_local_worker_on_overseas_control(
|
|
self,
|
|
mock_create_job,
|
|
mock_append_event,
|
|
mock_start_worker,
|
|
mock_send_worker_command,
|
|
mock_get_detect_status,
|
|
mock_get_settings_payload,
|
|
mock_dispatch_remote,
|
|
mock_create_snapshot,
|
|
) -> None:
|
|
mock_create_job.return_value = {
|
|
"job_id": 29,
|
|
"job_code": "sync-overseas-29",
|
|
"status": "running",
|
|
"items_pending": 100,
|
|
"items_claimed": 0,
|
|
"items_running": 0,
|
|
"task_mode": "single_step",
|
|
"step_code": "detect_register",
|
|
}
|
|
mock_get_detect_status.return_value = {
|
|
"worker_mode": "linux-systemd",
|
|
"worker_online": False,
|
|
"worker_process_count": 0,
|
|
"worker_latest_start_time": "",
|
|
"worker_runtime_message": "not-applicable",
|
|
"progress": {},
|
|
}
|
|
mock_get_settings_payload.return_value = {
|
|
"runtime": {"thread_count": 2000},
|
|
"proxy_config": {"proxy_enable": True, "allow_direct": False, "proxy_urls": ["a"]},
|
|
}
|
|
mock_dispatch_remote.return_value = {
|
|
"queued_jobs": [{"node_code": "mainland-controller-01"}],
|
|
"queued_total": 1,
|
|
"failed_total": 0,
|
|
"target_summary": {"controller_nodes": ["mainland-controller-01"], "worker_nodes": ["mainland-worker-01"]},
|
|
}
|
|
|
|
with patch.object(detect_route.settings, "node_region", "overseas"), patch.object(
|
|
detect_route.settings, "node_role", "control"
|
|
):
|
|
response = detect_route.start_detect()
|
|
|
|
self.assertEqual(0, response.code)
|
|
self.assertIn("海外控制面", response.message)
|
|
mock_start_worker.assert_not_called()
|
|
mock_send_worker_command.assert_not_called()
|
|
mock_dispatch_remote.assert_called_once()
|
|
mock_create_snapshot.assert_not_called()
|
|
event_types = [call.kwargs.get("event_type") for call in mock_append_event.call_args_list]
|
|
self.assertIn("job_dispatch_requested", event_types)
|
|
self.assertIn("job_dispatch_skipped_local", event_types)
|
|
|
|
@patch("app.api.routes.detect.finalize_detect_run")
|
|
@patch("app.api.routes.detect.mark_detect_run_stopping")
|
|
@patch("app.api.routes.detect.get_settings_payload")
|
|
@patch("app.api.routes.detect.get_detect_status")
|
|
@patch("app.api.routes.detect._dispatch_remote_detect_stop")
|
|
@patch("app.api.routes.detect.send_worker_command")
|
|
@patch("app.api.routes.detect.get_active_detect_job_summary")
|
|
def test_stop_detect_forwards_target_payload(
|
|
self,
|
|
mock_get_active_job,
|
|
mock_send_worker_command,
|
|
mock_dispatch_remote_stop,
|
|
mock_get_detect_status,
|
|
mock_get_settings_payload,
|
|
mock_mark_detect_run_stopping,
|
|
mock_finalize_detect_run,
|
|
) -> None:
|
|
mock_get_active_job.return_value = {
|
|
"job_id": 29,
|
|
"job_code": "sync-overseas-29",
|
|
"status": "running",
|
|
"current_cycle_token": "cycle-29",
|
|
}
|
|
mock_send_worker_command.return_value = (True, "已发送 Worker 控制指令")
|
|
mock_dispatch_remote_stop.return_value = {
|
|
"queued_jobs": [{"node_code": "mainland-controller-01"}],
|
|
"queued_total": 1,
|
|
"failed_total": 0,
|
|
"target_summary": {"controller_nodes": ["mainland-controller-01"], "worker_nodes": []},
|
|
}
|
|
mock_get_detect_status.return_value = {
|
|
"worker_mode": "linux-systemd",
|
|
"worker_online": True,
|
|
"worker_process_count": 1,
|
|
"worker_latest_start_time": "",
|
|
"worker_runtime_message": "running",
|
|
"progress": {},
|
|
}
|
|
mock_get_settings_payload.return_value = {
|
|
"thread_count": 1000,
|
|
"node_thread_counts": {},
|
|
"process_count": 80,
|
|
"node_process_counts": {},
|
|
"proxy_config": {"proxy_enable": True, "allow_direct": False, "proxy_urls": ["a"]},
|
|
}
|
|
|
|
response = detect_route.stop_detect(payload={"target_node_codes": ["mainland-controller-01"]})
|
|
|
|
self.assertEqual(0, response.code)
|
|
mock_send_worker_command.assert_called_once_with(
|
|
"stop_detection",
|
|
payload={"target_node_codes": ["mainland-controller-01"]},
|
|
)
|
|
mock_dispatch_remote_stop.assert_called_once_with(
|
|
active_job=mock_get_active_job.return_value,
|
|
cycle_token="cycle-29",
|
|
payload={"target_node_codes": ["mainland-controller-01"]},
|
|
)
|
|
mock_mark_detect_run_stopping.assert_called_once()
|
|
mock_finalize_detect_run.assert_not_called()
|
|
|
|
@patch("app.api.routes.detect.create_ops_job")
|
|
@patch("app.api.routes.detect.list_managed_nodes")
|
|
def test_dispatch_remote_detect_stop_filters_target_nodes(self, mock_list_managed_nodes, mock_create_ops_job) -> None:
|
|
mock_list_managed_nodes.return_value = [
|
|
{"node_code": "mainland-controller-01", "region": "mainland", "role": "control", "last_seen_at": "2026-04-23T14:00:00", "is_enabled": True},
|
|
{"node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "last_seen_at": "2026-04-23T14:00:00", "is_enabled": True},
|
|
]
|
|
mock_create_ops_job.return_value = (True, "queued", {})
|
|
|
|
result = detect_route._dispatch_remote_detect_stop(
|
|
active_job={"job_id": 29, "job_code": "sync-overseas-29"},
|
|
cycle_token="cycle-29",
|
|
payload={"target_node_codes": ["mainland-worker-01"]},
|
|
)
|
|
|
|
self.assertEqual(1, result["queued_total"])
|
|
self.assertEqual(["mainland-controller-01"], result["target_summary"]["controller_nodes"])
|
|
self.assertEqual(["mainland-worker-01"], result["target_summary"]["worker_nodes"])
|
|
mock_create_ops_job.assert_called_once()
|
|
create_payload = mock_create_ops_job.call_args.args[0]
|
|
self.assertEqual("mainland-worker-01", create_payload["target_node_code"])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|