import json import unittest from datetime import datetime from unittest.mock import MagicMock, patch from app.core.config import settings from app.core.db import get_db from app.services.ops_job_service import ( _execute_control_plane_job, _serialize_job_row, create_ops_job, dispatch_ops_job, list_managed_nodes, upsert_managed_node, ) class OpsJobServiceTests(unittest.TestCase): def test_serialize_job_row_adds_execution_mode_label(self) -> None: row = ( 9, "ops-2026041801-abcd12", "logs.collect", "node", "mainland-worker-01", "queued", "remote-agent", "api", json.dumps({"service_name": "domaincheck-worker", "lines": 120}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), "", datetime(2026, 4, 18, 10, 0, 0), None, None, datetime(2026, 4, 18, 10, 0, 0), "low", False, "approved", "", None, "", "", None, json.dumps({}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), None, ) job = _serialize_job_row(row) self.assertEqual("remote-agent", job["execution_mode"]) self.assertEqual("远端 Agent", job["execution_mode_label"]) self.assertEqual("排队中", job["status_label"]) self.assertEqual("已审批", job["approval_status_label"]) self.assertEqual(["mainland-worker-01"], job["target_node_codes"]) self.assertEqual(0, job["steps_total"]) self.assertFalse(job["steps_loaded"]) self.assertFalse(job["is_compact"]) self.assertEqual("ops_job", job["focus_ref"]["kind"]) self.assertEqual(9, job["focus_ref"]["job_id"]) self.assertIn("目标节点 mainland-worker-01", job["summary_text"]) def test_serialize_job_row_counts_loaded_steps(self) -> None: row = ( 10, "ops-2026041801-efgh34", "diagnostics.collect", "node", "mainland-worker-02", "running", "ssh", "web-ui", json.dumps({}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), "", datetime(2026, 4, 18, 10, 5, 0), datetime(2026, 4, 18, 10, 5, 2), None, datetime(2026, 4, 18, 10, 5, 3), "medium", True, "approved", "admin", datetime(2026, 4, 18, 10, 5, 1), "", "", datetime(2026, 4, 18, 10, 5, 2), json.dumps({}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), None, ) job = _serialize_job_row( row, steps=[ {"id": 1, "status": "success"}, {"id": 2, "status": "running"}, {"id": 3, "status": "failed"}, ], ) self.assertEqual("执行中", job["status_label"]) self.assertEqual("已审批", job["approval_status_label"]) self.assertEqual(3, job["steps_total"]) self.assertEqual(1, job["steps_running"]) self.assertEqual(1, job["steps_success"]) self.assertEqual(1, job["steps_failed"]) self.assertEqual(2, job["steps_terminal"]) self.assertTrue(job["steps_loaded"]) self.assertIn("mainland-worker-02", job["summary_text"]) def test_upsert_managed_node_supports_ssh_entry_and_secret_flags(self) -> None: node_code = "test-managed-node-secret" ok, message, data = upsert_managed_node( { "node_code": node_code, "region": "mainland", "role": "worker", "ssh_entry": "121.204.244.188 root ", "is_enabled": True, } ) self.assertTrue(ok, message) node = data["node"] self.assertEqual("121.204.244.188", node["ssh_host"]) self.assertEqual("root", node["ssh_user"]) self.assertEqual(22, node["ssh_port"]) self.assertEqual("password", node["auth_mode"]) self.assertTrue(node["ssh_password_configured"]) managed = next(item for item in list_managed_nodes() if item["node_code"] == node_code) self.assertTrue(managed["ssh_password_configured"]) with get_db() as conn: with conn.cursor() as cur: cur.execute( "SELECT ssh_password FROM ops_managed_node_secrets WHERE node_code = %s", (node_code,), ) row = cur.fetchone() self.assertIsNotNone(row) self.assertEqual("demo-password", str(row[0] or "")) @patch("app.services.ops_job_service.list_managed_nodes") @patch("app.services.ops_agent_service.build_node_agent_bootstrap_plan") def test_execute_control_plane_bootstrap_uses_managed_node_defaults( self, mock_build_node_agent_bootstrap_plan, mock_list_managed_nodes, ) -> None: mock_list_managed_nodes.return_value = [ { "node_code": "mainland-worker-02", "region": "mainland", "role": "worker", } ] mock_build_node_agent_bootstrap_plan.return_value = ( True, "节点 Agent 接入方案已生成", { "token_preview": "agent-...1234", "bootstrap_plan": { "bootstrap_script_name": "bootstrap-node-agent-mainland-worker-02.sh", }, }, ) ok, message, data = _execute_control_plane_job( "node.bootstrap", target_node_code="mainland-worker-02", payload={ "root_dir": "/opt/domaincheck", "expires_in_hours": 12, }, requested_by="unit-test", metadata={"source": "test"}, ) self.assertTrue(ok) self.assertEqual("节点 Agent 接入方案已生成", message) self.assertEqual("control-plane", data["execution_mode"]) self.assertEqual("mainland", data["node_region"]) self.assertEqual("worker", data["node_role"]) mock_build_node_agent_bootstrap_plan.assert_called_once_with( node_code="mainland-worker-02", node_region="mainland", node_role="worker", issued_by="unit-test", expires_in_hours=12, control_plane_base_url="", root_dir="/opt/domaincheck", metadata={ "source": "test", "issued_from": "ops-job/control-plane", "target_node_code": "mainland-worker-02", "node_region": "mainland", "node_role": "worker", }, ) @patch("app.services.ops_job_service.get_db") def test_upsert_managed_node_preserves_remote_fields_when_sync_payload_omits_them(self, mock_get_db) -> None: existing_metadata = {"operator_note": "keep-me"} merged_metadata = {"operator_note": "keep-me", "cluster_status": "online"} existing_row = ( "mainland-worker-01", "mainland", "worker", "Mainland Worker 01", "121.204.244.248", 22, "root", "key", "stable", False, json.dumps(existing_metadata, ensure_ascii=False), datetime(2026, 4, 18, 1, 0, 0), None, datetime(2026, 4, 18, 1, 5, 0), ) returned_row = ( "mainland-worker-01", "mainland", "worker", "S244-248", "121.204.244.248", 22, "root", "key", "stable", False, json.dumps(merged_metadata, ensure_ascii=False), datetime(2026, 4, 18, 1, 0, 0), None, datetime(2026, 4, 18, 1, 6, 0), ) conn = MagicMock() cursor = MagicMock() db_ctx = MagicMock() cursor_ctx = MagicMock() db_ctx.__enter__.return_value = conn db_ctx.__exit__.return_value = False cursor_ctx.__enter__.return_value = cursor cursor_ctx.__exit__.return_value = False conn.cursor.return_value = cursor_ctx mock_get_db.return_value = db_ctx cursor.fetchone.side_effect = [existing_row, returned_row] ok, message, data = upsert_managed_node( { "node_code": "mainland-worker-01", "region": "mainland", "role": "worker", "title": "S244-248", "metadata": { "cluster_status": "online", }, } ) self.assertTrue(ok) self.assertEqual("托管节点已保存", message) node = data["node"] self.assertEqual("121.204.244.248", node["ssh_host"]) self.assertEqual("root", node["ssh_user"]) self.assertFalse(node["is_enabled"]) self.assertEqual("keep-me", node["metadata"]["operator_note"]) self.assertEqual("online", node["metadata"]["cluster_status"]) insert_call = next( call for call in cursor.execute.call_args_list if "INSERT INTO ops_managed_nodes" in str(call.args[0]) ) insert_params = insert_call.args[1] self.assertEqual("121.204.244.248", insert_params[4]) self.assertEqual(22, insert_params[5]) self.assertEqual("root", insert_params[6]) self.assertFalse(insert_params[9]) self.assertEqual(merged_metadata, json.loads(insert_params[10])) @patch("app.services.ops_agent_service.append_ops_job_event") @patch("app.services.ops_job_service._execute_ssh_job_record") @patch("app.services.ops_job_service.get_db") def test_dispatch_ops_job_ssh_executes_via_ssh_record( self, mock_get_db, mock_execute_ssh_job_record, mock_append_ops_job_event, ) -> None: conn = MagicMock() cursor = MagicMock() db_ctx = MagicMock() cursor_ctx = MagicMock() db_ctx.__enter__.return_value = conn db_ctx.__exit__.return_value = False cursor_ctx.__enter__.return_value = cursor cursor_ctx.__exit__.return_value = False conn.cursor.return_value = cursor_ctx mock_get_db.return_value = db_ctx cursor.fetchone.return_value = ( 9, "ops-2026041801-abcd12", "logs.collect", "node", "mainland-worker-01", "queued", "ssh", "api", json.dumps({"service_name": "domaincheck-worker", "lines": 120}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), "", datetime(2026, 4, 18, 10, 0, 0), None, None, datetime(2026, 4, 18, 10, 0, 0), "low", False, "approved", "", None, "", "", None, json.dumps({}, ensure_ascii=False), json.dumps({}, ensure_ascii=False), None, ) mock_execute_ssh_job_record.return_value = ( True, "worker logs collected", {"job": {"id": 9, "status": "success"}}, ) ok, message, data = dispatch_ops_job(9) self.assertTrue(ok) self.assertEqual("已按 SSH 执行派发:worker logs collected", message) self.assertEqual("success", data["job"]["status"]) mock_execute_ssh_job_record.assert_called_once_with(9) mock_append_ops_job_event.assert_called_once() event_kwargs = mock_append_ops_job_event.call_args.kwargs self.assertEqual("job_dispatch_requested", event_kwargs["event_type"]) self.assertEqual("mainland-worker-01", event_kwargs["node_code"]) self.assertEqual("ssh", event_kwargs["payload"]["execution_mode"]) @patch("app.services.ops_agent_service.append_ops_job_event") @patch("app.services.ops_job_service.get_ops_job") @patch("app.services.ops_job_service._execute_local_job") @patch("app.services.ops_policy_service.preview_ops_job_policy") @patch("app.services.ops_job_service.get_db") def test_create_ops_job_auto_uses_local_runtime_for_local_structured_action( self, mock_get_db, mock_preview_ops_job_policy, mock_execute_local_job, mock_get_ops_job, mock_append_ops_job_event, ) -> None: conn = MagicMock() cursor = MagicMock() db_ctx = MagicMock() cursor_ctx = MagicMock() db_ctx.__enter__.return_value = conn db_ctx.__exit__.return_value = False cursor_ctx.__enter__.return_value = cursor cursor_ctx.__exit__.return_value = False conn.cursor.return_value = cursor_ctx mock_get_db.return_value = db_ctx cursor.fetchone.return_value = (42,) mock_preview_ops_job_policy.return_value = { "risk_level": "low", "approval_required": False, "blocked": False, "blocking_reasons": [], } mock_execute_local_job.return_value = ( True, "worker logs collected", {"executor": "local-runtime"}, ) mock_get_ops_job.return_value = { "id": 42, "status": "success", "execution_mode": "local-runtime", } ok, message, data = create_ops_job( { "action": "logs.collect", "target_node_code": settings.node_code, "payload": { "service_name": "domaincheck-worker", "lines": 120, }, "requested_by": "unit-test", } ) self.assertTrue(ok) self.assertEqual("worker logs collected", message) self.assertTrue(data["executed_immediately"]) self.assertEqual("local-runtime", data["job"]["execution_mode"]) mock_execute_local_job.assert_called_once_with( "logs.collect", { "service_name": "domaincheck-worker", "lines": 120, }, ) insert_call = next( call for call in cursor.execute.call_args_list if "INSERT INTO ops_jobs" in str(call.args[0]) ) insert_params = insert_call.args[1] self.assertEqual("local-runtime", insert_params[5]) mock_append_ops_job_event.assert_called_once() if __name__ == "__main__": unittest.main()