This commit is contained in:
Your Name
2026-04-22 14:13:21 +08:00
parent e0406b5d0e
commit 7cbde2aa78
145 changed files with 23086 additions and 2243 deletions

View File

@@ -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()