d
This commit is contained in:
@@ -4,8 +4,11 @@ from unittest.mock import patch
|
||||
|
||||
from app.api.routes.ops_agent import _build_agent_response
|
||||
from app.services.ops_agent_service import (
|
||||
_build_agent_runtime_config_bundle,
|
||||
_runtime_config_bundle_hash_payload,
|
||||
agent_append_job_event,
|
||||
agent_complete_job,
|
||||
agent_mark_job_started,
|
||||
agent_pull_jobs,
|
||||
agent_register,
|
||||
build_node_agent_bootstrap_plan,
|
||||
@@ -102,6 +105,64 @@ class _SequenceConnection:
|
||||
|
||||
|
||||
class OpsAgentServiceTests(unittest.TestCase):
|
||||
def test_runtime_config_bundle_hash_ignores_generated_at(self) -> None:
|
||||
bundle_a = {
|
||||
"node_code": "mainland-worker-01",
|
||||
"thread_count": 100,
|
||||
"node_thread_counts": {"mainland-worker-01": 100},
|
||||
"runtime_settings": {"worker_log_sync_enabled": True},
|
||||
"generated_at": "2026-04-19 17:30:00",
|
||||
"config_hash": "stale-hash",
|
||||
}
|
||||
bundle_b = {
|
||||
"node_code": "mainland-worker-01",
|
||||
"thread_count": 100,
|
||||
"node_thread_counts": {"mainland-worker-01": 100},
|
||||
"runtime_settings": {"worker_log_sync_enabled": True},
|
||||
"generated_at": "2026-04-19 17:35:00",
|
||||
"config_hash": "other-stale-hash",
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
_runtime_config_bundle_hash_payload(bundle_a),
|
||||
_runtime_config_bundle_hash_payload(bundle_b),
|
||||
)
|
||||
|
||||
@patch("app.services.ops_agent_service.get_sensitive_words_payload")
|
||||
@patch("app.services.ops_agent_service.get_runtime_settings")
|
||||
@patch("app.services.ops_agent_service.get_settings_payload")
|
||||
def test_build_agent_runtime_config_bundle_keeps_hash_stable_for_same_config(
|
||||
self,
|
||||
mock_get_settings_payload,
|
||||
mock_get_runtime_settings,
|
||||
mock_get_sensitive_words_payload,
|
||||
) -> None:
|
||||
mock_get_settings_payload.return_value = {
|
||||
"detect_options": {"detect_wayback": True},
|
||||
"proxy_config": {"enabled": True},
|
||||
"thread_count": 100,
|
||||
"node_thread_counts": {"mainland-worker-01": 100},
|
||||
}
|
||||
mock_get_runtime_settings.return_value = {
|
||||
"worker_log_sync_enabled": True,
|
||||
"worker_log_sync_mode": "full",
|
||||
}
|
||||
mock_get_sensitive_words_payload.return_value = {
|
||||
"text": "foo\nbar",
|
||||
"total": 2,
|
||||
"items": ["foo", "bar"],
|
||||
}
|
||||
|
||||
with patch(
|
||||
"app.services.ops_agent_service._format_time",
|
||||
side_effect=["2026-04-19 17:30:00", "2026-04-19 17:35:00"],
|
||||
):
|
||||
first_bundle = _build_agent_runtime_config_bundle("mainland-worker-01")
|
||||
second_bundle = _build_agent_runtime_config_bundle("mainland-worker-01")
|
||||
|
||||
self.assertNotEqual(first_bundle["generated_at"], second_bundle["generated_at"])
|
||||
self.assertEqual(first_bundle["config_hash"], second_bundle["config_hash"])
|
||||
|
||||
def test_build_agent_response_extracts_detail_code(self) -> None:
|
||||
response = _build_agent_response(False, "bad request", {"detail_code": "agent_token_invalid", "foo": "bar"})
|
||||
|
||||
@@ -730,6 +791,76 @@ class OpsAgentServiceTests(unittest.TestCase):
|
||||
self.assertEqual(0, payload["summary"]["agent_ready"])
|
||||
self.assertEqual(1, payload["summary"]["remote_access_ready"])
|
||||
|
||||
@patch("app.services.runtime_status_service.get_runtime_status")
|
||||
@patch("app.services.cluster_runtime_service.get_cluster_snapshot")
|
||||
@patch("app.services.ops_job_service.list_managed_nodes")
|
||||
@patch("app.services.ops_agent_service.get_db")
|
||||
@patch("app.services.ops_agent_service.ensure_ops_agent_schema")
|
||||
def test_list_managed_nodes_with_agent_state_prefers_non_loopback_cluster_identity_and_infers_worker_online(
|
||||
self,
|
||||
mock_ensure_ops_agent_schema,
|
||||
mock_get_db,
|
||||
mock_list_managed_nodes,
|
||||
mock_get_cluster_snapshot,
|
||||
mock_get_runtime_status,
|
||||
) -> None:
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
mock_ensure_ops_agent_schema.return_value = None
|
||||
mock_get_db.return_value = _EmptyConnection()
|
||||
mock_list_managed_nodes.return_value = [
|
||||
{
|
||||
"node_code": "mainland-worker-01",
|
||||
"region": "mainland",
|
||||
"role": "worker",
|
||||
"title": "Mainland Worker 01",
|
||||
"is_enabled": True,
|
||||
"metadata": {
|
||||
"hostname": "S244-248",
|
||||
"ip": "121.204.244.248",
|
||||
},
|
||||
"last_seen_at": now,
|
||||
}
|
||||
]
|
||||
mock_get_cluster_snapshot.return_value = {
|
||||
"nodes": [
|
||||
{
|
||||
"node_code": "mainland-worker-01",
|
||||
"region": "mainland",
|
||||
"role": "worker",
|
||||
"status": "busy",
|
||||
"current_load": 19,
|
||||
"hostname": "localhost",
|
||||
"ip": "127.0.0.1",
|
||||
"last_heartbeat_at": now,
|
||||
"is_effective_worker": True,
|
||||
"detect_participating": False,
|
||||
"metadata": {
|
||||
"worker_online": False,
|
||||
"active_threads": 19,
|
||||
"max_threads": 1200,
|
||||
"phase_label": "检测中",
|
||||
"phase_detail": "Worker 正在处理 162 个检测任务",
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
mock_get_runtime_status.return_value = {
|
||||
"detect": {
|
||||
"participating_nodes": [],
|
||||
"non_participating_nodes": [],
|
||||
}
|
||||
}
|
||||
|
||||
payload = list_managed_nodes_with_agent_state()
|
||||
|
||||
self.assertEqual(1, len(payload["nodes"]))
|
||||
row = payload["nodes"][0]
|
||||
self.assertEqual("S244-248", row["cluster_hostname"])
|
||||
self.assertEqual("121.204.244.248", row["cluster_ip"])
|
||||
self.assertTrue(row["detect_runtime"]["worker_online"])
|
||||
self.assertTrue(row["detect_runtime"]["detect_participating"])
|
||||
self.assertEqual(19, row["detect_runtime"]["active_threads"])
|
||||
|
||||
def test_agent_register_requires_node_code_with_detail_code(self) -> None:
|
||||
ok, message, data = agent_register({}, token="ignored")
|
||||
|
||||
@@ -1176,6 +1307,119 @@ class OpsAgentServiceTests(unittest.TestCase):
|
||||
self.assertEqual("当前控制面仅支持对可见头部记录执行单条动作", message)
|
||||
self.assertEqual("evt-hidden", data["record_id"])
|
||||
|
||||
@patch("app.services.ops_agent_service.get_ops_job")
|
||||
@patch("app.services.ops_agent_service.append_ops_job_event")
|
||||
@patch("app.services.ops_agent_service._authenticate_agent_token")
|
||||
@patch("app.services.ops_agent_service.get_db")
|
||||
def test_agent_pull_jobs_commits_before_appending_event(
|
||||
self,
|
||||
mock_get_db,
|
||||
mock_authenticate_agent_token,
|
||||
mock_append_ops_job_event,
|
||||
mock_get_ops_job,
|
||||
) -> None:
|
||||
cursor = _SequenceCursor(fetchall_values=[[(36,)]] )
|
||||
connection = _SequenceConnection(cursor)
|
||||
mock_get_db.return_value = connection
|
||||
mock_authenticate_agent_token.return_value = (True, "ok", {"node_code": "mainland-worker-01"})
|
||||
mock_get_ops_job.return_value = {"id": 36, "job_code": "ops-demo", "action": "health.snapshot"}
|
||||
|
||||
def _assert_after_commit(**kwargs):
|
||||
self.assertTrue(connection.committed)
|
||||
self.assertEqual(36, kwargs["job_id"])
|
||||
self.assertEqual("agent_dispatched", kwargs["event_type"])
|
||||
|
||||
mock_append_ops_job_event.side_effect = _assert_after_commit
|
||||
|
||||
ok, message, data = agent_pull_jobs({"node_code": "mainland-worker-01"}, token="agent-token", limit=1)
|
||||
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual("ok", message)
|
||||
self.assertEqual(1, data["count"])
|
||||
self.assertTrue(connection.committed)
|
||||
mock_append_ops_job_event.assert_called_once()
|
||||
|
||||
@patch("app.services.ops_agent_service.get_ops_job")
|
||||
@patch("app.services.ops_agent_service.append_ops_job_event")
|
||||
@patch("app.services.ops_agent_service._authenticate_agent_token")
|
||||
@patch("app.services.ops_agent_service.get_db")
|
||||
def test_agent_mark_job_started_commits_before_appending_event(
|
||||
self,
|
||||
mock_get_db,
|
||||
mock_authenticate_agent_token,
|
||||
mock_append_ops_job_event,
|
||||
mock_get_ops_job,
|
||||
) -> None:
|
||||
cursor = _SequenceCursor(fetchone_values=[(36,)], fetchall_values=[[(101,), (102,)]])
|
||||
connection = _SequenceConnection(cursor)
|
||||
mock_get_db.return_value = connection
|
||||
mock_authenticate_agent_token.return_value = (True, "ok", {"node_code": "mainland-worker-01"})
|
||||
mock_get_ops_job.return_value = {"id": 36, "job_code": "ops-demo", "action": "health.snapshot"}
|
||||
|
||||
def _assert_after_commit(**kwargs):
|
||||
self.assertTrue(connection.committed)
|
||||
self.assertEqual(36, kwargs["job_id"])
|
||||
self.assertEqual("agent_started", kwargs["event_type"])
|
||||
self.assertEqual({"step_ids": [101, 102]}, kwargs["payload"])
|
||||
|
||||
mock_append_ops_job_event.side_effect = _assert_after_commit
|
||||
|
||||
ok, message, data = agent_mark_job_started(36, {"node_code": "mainland-worker-01"}, token="agent-token")
|
||||
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual("任务已标记为运行中", message)
|
||||
self.assertEqual(36, data["job"]["id"])
|
||||
self.assertTrue(connection.committed)
|
||||
mock_append_ops_job_event.assert_called_once()
|
||||
|
||||
@patch("app.services.ops_release_service.refresh_release_rollout_for_job")
|
||||
@patch("app.services.ops_agent_service.get_ops_job")
|
||||
@patch("app.services.ops_agent_service.append_ops_job_event")
|
||||
@patch("app.services.ops_agent_service._authenticate_agent_token")
|
||||
@patch("app.services.ops_agent_service.get_db")
|
||||
def test_agent_complete_job_commits_before_appending_event(
|
||||
self,
|
||||
mock_get_db,
|
||||
mock_authenticate_agent_token,
|
||||
mock_append_ops_job_event,
|
||||
mock_get_ops_job,
|
||||
mock_refresh_release_rollout_for_job,
|
||||
) -> None:
|
||||
cursor = _SequenceCursor(fetchone_values=[(36, "")], fetchall_values=[[(101,)]] )
|
||||
connection = _SequenceConnection(cursor)
|
||||
mock_get_db.return_value = connection
|
||||
mock_authenticate_agent_token.return_value = (True, "ok", {"node_code": "mainland-worker-01"})
|
||||
mock_get_ops_job.return_value = {"id": 36, "job_code": "ops-demo", "status": "success"}
|
||||
|
||||
def _assert_after_commit(**kwargs):
|
||||
self.assertTrue(connection.committed)
|
||||
self.assertEqual(36, kwargs["job_id"])
|
||||
self.assertEqual("agent_completed", kwargs["event_type"])
|
||||
self.assertEqual("info", kwargs["level"])
|
||||
self.assertEqual([101], kwargs["payload"]["step_ids"])
|
||||
|
||||
mock_append_ops_job_event.side_effect = _assert_after_commit
|
||||
|
||||
ok, message, data = agent_complete_job(
|
||||
36,
|
||||
{
|
||||
"node_code": "mainland-worker-01",
|
||||
"status": "success",
|
||||
"stdout": "ok",
|
||||
"stderr": "",
|
||||
"result": {"summary": "done"},
|
||||
"duration_ms": 120,
|
||||
},
|
||||
token="agent-token",
|
||||
)
|
||||
|
||||
self.assertTrue(ok)
|
||||
self.assertEqual("任务结果已回写", message)
|
||||
self.assertEqual(36, data["job"]["id"])
|
||||
self.assertTrue(connection.committed)
|
||||
mock_append_ops_job_event.assert_called_once()
|
||||
mock_refresh_release_rollout_for_job.assert_called_once_with(36)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user