359 lines
20 KiB
Python
359 lines
20 KiB
Python
import unittest
|
||
from unittest.mock import patch
|
||
|
||
from app.services.ops_service import get_ops_driver_feed
|
||
|
||
|
||
class OpsServiceDriverFeedTests(unittest.TestCase):
|
||
@patch("app.services.ops_service.get_ops_go_live_summary")
|
||
@patch("app.services.ops_service.get_ops_runbook")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
def test_driver_feed_merges_priority_recommendation_driver_cards_and_runbook_sequences(
|
||
self,
|
||
mock_get_ops_overview,
|
||
mock_get_ops_runbook,
|
||
mock_get_ops_go_live_summary,
|
||
) -> None:
|
||
mock_get_ops_go_live_summary.return_value = {
|
||
"go_live_status": "blocked",
|
||
"publish_ready": False,
|
||
"publish_status": "blocked",
|
||
"publish_status_label": "暂不可发布",
|
||
"publish_summary": "当前仍有硬阻断项,不能直接进入正式发版。",
|
||
"operator_title": "先补接管缺口",
|
||
"next_step_action_code": "handover_first_gap",
|
||
"launchpad_recommended_target_node_code": "mainland-controller-01",
|
||
"launchpad_recommended_recovery_label": "签发接入工单",
|
||
"launchpad_recommended_recovery_summary": "当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap。",
|
||
"launchpad_onboarding_bootstrap_pending_nodes": 1,
|
||
"launchpad_onboarding_acceptance_ready_nodes": 1,
|
||
"blocking_reasons": ["remote_agent_ready=0"],
|
||
"warnings": ["log_sync_waiting_sample=0/2"],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"execution_scene": {
|
||
"participation_summary": {
|
||
"participating_nodes": 2,
|
||
"dispatch_active_node_codes": ["mainland-worker-01", "mainland-controller-01"],
|
||
},
|
||
"participating_nodes": [
|
||
{"node_code": "mainland-worker-01"},
|
||
{"node_code": "mainland-controller-01"},
|
||
],
|
||
"log_sync": {
|
||
"enabled": False,
|
||
"mode": "key",
|
||
"line_count": 0,
|
||
"source_node_count": 0,
|
||
"participating_node_count": 2,
|
||
"covered_participating_node_count": 0,
|
||
"missing_participating_nodes": ["mainland-worker-01", "mainland-controller-01"],
|
||
},
|
||
},
|
||
"recommendation": {
|
||
"key": "fix-handover-gap",
|
||
"priority": "先补接管缺口",
|
||
"title": "先补接管缺口",
|
||
"summary": "当前应该先把接管链路补齐。",
|
||
"reason": "否则后续批量动作无法稳定下发。",
|
||
"level_label": "最高优先",
|
||
"tag_type": "danger",
|
||
"primary_label": "纳管首台节点",
|
||
"secondary_label": "查看目标节点",
|
||
"primary_action_code": "handover_first_gap",
|
||
"secondary_action_code": "view_first_gap",
|
||
"node_codes": ["mainland-worker-02"],
|
||
"primary_node_codes": ["mainland-worker-02"],
|
||
"secondary_node_codes": ["mainland-worker-02"],
|
||
"primary_action_payload": {"requested_by": "test"},
|
||
"secondary_action_payload": {},
|
||
"reasons": ["当前存在接管缺口。"],
|
||
},
|
||
"activity_stream": {
|
||
"items": [
|
||
{
|
||
"kind": "ops_job",
|
||
"activity_key": "ops-job:501",
|
||
"title": "logs.collect",
|
||
"subtitle": "ops-logs-501",
|
||
"summary_text": "远端 Agent 已回传 120 行 Worker 日志,可直接继续分析。",
|
||
"summary": "远端 Agent 已回传 120 行 Worker 日志,可直接继续分析。",
|
||
"status": "running",
|
||
"status_label": "执行中",
|
||
"meta_text": "ops-logs-501 / 执行 远端 Agent",
|
||
"occurred_at": "2026-04-18 09:18:00",
|
||
"target_node_codes": ["mainland-worker-01"],
|
||
"focus_ref": {
|
||
"kind": "ops_job",
|
||
"job_id": 501,
|
||
"job_code": "ops-logs-501",
|
||
"action": "logs.collect",
|
||
"target_node_code": "mainland-worker-01",
|
||
},
|
||
"source_focus_ref": {
|
||
"kind": "ops_job_event",
|
||
"job_id": 501,
|
||
"event_key": "job-event:501",
|
||
},
|
||
"ui_intent": {
|
||
"kind": "job_events",
|
||
"job_id": 501,
|
||
"job_code": "ops-logs-501",
|
||
"action": "logs.collect",
|
||
"target_node_code": "mainland-worker-01",
|
||
},
|
||
}
|
||
]
|
||
},
|
||
"driver_recommendations": [
|
||
{
|
||
"key": "fix-handover-gap",
|
||
"title": "先补接管缺口",
|
||
"summary": "重复建议,不应再次进入 feed。",
|
||
"reason": "duplicate",
|
||
"level_label": "最高优先",
|
||
"tag_type": "danger",
|
||
"primary_label": "纳管首台节点",
|
||
"primary_action_code": "handover_first_gap",
|
||
},
|
||
{
|
||
"key": "inspect-scene-log-gap",
|
||
"title": "排查现场日志样本缺口",
|
||
"summary": "现场日志样本还没有形成。",
|
||
"reason": "先看 Worker 日志。",
|
||
"meta_text": "参与节点 2 台 / 已回传来源 0 台",
|
||
"level_label": "推荐",
|
||
"tag_type": "warning",
|
||
"primary_label": "看 Worker 日志",
|
||
"secondary_label": "执行标准巡检",
|
||
"primary_action_code": "open_worker_logs",
|
||
"secondary_action_code": "run_standard_inspection",
|
||
"node_codes": ["mainland-worker-01", "mainland-controller-01"],
|
||
"primary_node_codes": ["mainland-worker-01", "mainland-controller-01"],
|
||
"secondary_node_codes": ["mainland-worker-01", "mainland-controller-01"],
|
||
"primary_action_payload": {"lines": 120},
|
||
"secondary_action_payload": {},
|
||
},
|
||
],
|
||
}
|
||
mock_get_ops_runbook.return_value = {
|
||
"control_sequences": [
|
||
{
|
||
"key": "release_progression",
|
||
"title": "版本发布与放量",
|
||
"summary": "当前可进入发布路径。",
|
||
"reason": "正式运维时应统一落回 Release / Rollout。",
|
||
"status": "warning",
|
||
"status_label": "待处理",
|
||
"tag_type": "warning",
|
||
"target_scope_label": "Release / Rollout",
|
||
"target_node_codes": ["mainland-controller-01"],
|
||
"focus_ref": {
|
||
"kind": "release_hub",
|
||
"release_id": 7,
|
||
"section": "release_launchpad",
|
||
},
|
||
"primary_label": "补执行面接管",
|
||
"secondary_label": "查看版本区",
|
||
"primary_action_code": "fix_managed_nodes",
|
||
"secondary_action_code": "focus_release_hub",
|
||
"primary_focus_ref": {
|
||
"kind": "release_hub",
|
||
"release_id": 7,
|
||
"section": "default_rollout_gate",
|
||
},
|
||
"secondary_focus_ref": {
|
||
"kind": "release_hub",
|
||
"release_id": 7,
|
||
"section": "release_detail",
|
||
},
|
||
"step_titles": ["同步集群节点", "纳管首台缺口", "补齐 Agent 接入"],
|
||
"primary_resolution": {
|
||
"ok": True,
|
||
"driver_action_code": "fix_managed_nodes",
|
||
"driver_action_label": "补执行面接管",
|
||
"driver_node_codes": ["mainland-controller-01"],
|
||
"driver_action_payload": {"channel": "stable"},
|
||
"resolved_at": "2026-04-18T03:00:00",
|
||
},
|
||
"secondary_resolution": {
|
||
"ok": True,
|
||
"driver_action_code": "focus_release_hub",
|
||
"driver_action_label": "查看版本区",
|
||
"driver_node_codes": [],
|
||
"driver_action_payload": {"release_id": 7},
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
payload = get_ops_driver_feed()
|
||
|
||
self.assertEqual(3, payload["summary"]["total"])
|
||
self.assertEqual("blocked", payload["go_live_summary"]["go_live_status"])
|
||
self.assertEqual("blocked", payload["summary"]["go_live_status"])
|
||
self.assertEqual("先补接管缺口", payload["summary"]["operator_title"])
|
||
self.assertEqual("blocked", payload["summary"]["publish_status"])
|
||
self.assertFalse(payload["summary"]["publish_ready"])
|
||
self.assertEqual("暂不可发布", payload["summary"]["publish_status_label"])
|
||
self.assertIn("发布闸门:暂不可发布", payload["headline"])
|
||
self.assertIn("接入缺口:签发接入工单:mainland-controller-01", payload["headline"])
|
||
self.assertEqual("handover_first_gap", payload["go_live_summary"]["next_step_action_code"])
|
||
self.assertEqual("mainland-controller-01", payload["summary"]["launchpad_recommended_target_node_code"])
|
||
self.assertEqual("签发接入工单", payload["summary"]["launchpad_recommended_recovery_label"])
|
||
self.assertEqual(
|
||
"当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap。",
|
||
payload["summary"]["launchpad_recommended_recovery_summary"],
|
||
)
|
||
self.assertEqual(1, payload["summary"]["launchpad_onboarding_bootstrap_pending_nodes"])
|
||
self.assertEqual(1, payload["summary"]["launchpad_onboarding_acceptance_ready_nodes"])
|
||
self.assertEqual(2, payload["summary"]["do_now_total"])
|
||
self.assertEqual(1, payload["summary"]["standard_path_total"])
|
||
self.assertEqual("ops_driver_contract", payload["contract_navigation"]["primary_contract_key"])
|
||
self.assertIn("ops_playbook_contract", payload["contract_navigation"]["contract_keys"])
|
||
self.assertIn("ops_observability_contract", payload["contract_navigation"]["contract_keys"])
|
||
self.assertEqual("priority:fix-handover-gap", payload["top_recommendation"]["key"])
|
||
self.assertEqual("ops_driver_contract", payload["top_recommendation"]["contract_navigation"]["primary_contract_key"])
|
||
self.assertEqual(2, len(payload["driver_recommendations"]))
|
||
self.assertEqual(1, len(payload["runbook_sequences"]))
|
||
self.assertEqual(1, len(payload["activity_focus"]))
|
||
self.assertEqual(
|
||
["priority_recommendation", "driver_recommendation", "runbook_sequence"],
|
||
[item["kind"] for item in payload["entries"]],
|
||
)
|
||
self.assertEqual(1, payload["summary"]["activity_focus_total"])
|
||
self.assertEqual("disabled", payload["summary"]["scene_log_status"])
|
||
self.assertEqual("blocked", payload["summary"]["launch_status"])
|
||
self.assertFalse(payload["summary"]["launch_ready"])
|
||
self.assertEqual(2, payload["summary"]["preview_only_total"])
|
||
self.assertEqual(3, payload["summary"]["backend_handled_total"])
|
||
self.assertEqual("blocked", payload["automation_coverage"]["launch_status"])
|
||
self.assertEqual(2, payload["automation_coverage"]["safe_auto_total"])
|
||
self.assertEqual(0, payload["automation_coverage"]["guarded_auto_total"])
|
||
self.assertEqual(2, payload["automation_coverage"]["auto_execute_total"])
|
||
self.assertEqual(1, payload["automation_coverage"]["resolve_first_total"])
|
||
self.assertEqual(2, payload["automation_coverage"]["preview_only_total"])
|
||
self.assertEqual(3, payload["automation_coverage"]["backend_handled_total"])
|
||
self.assertEqual(1, payload["automation_coverage"]["human_dependency_total"])
|
||
self.assertFalse(payload["automation_coverage"]["launch_ready"])
|
||
|
||
priority_entry = payload["entries"][0]
|
||
self.assertEqual("do_now", priority_entry["lane"])
|
||
self.assertEqual("driver_action", priority_entry["executor_kind"])
|
||
self.assertEqual("handover_first_gap", priority_entry["primary_action_code"])
|
||
self.assertEqual("ops_driver_contract", priority_entry["contract_navigation"]["primary_contract_key"])
|
||
|
||
activity_focus = payload["activity_focus"][0]
|
||
self.assertEqual("ops_job", activity_focus["activity_kind"])
|
||
self.assertEqual("远端 Agent 已回传 120 行 Worker 日志,可直接继续分析。", activity_focus["summary"])
|
||
self.assertEqual("focus_latest_job_events", activity_focus["focus_action_code"])
|
||
self.assertEqual("job_events", activity_focus["ui_intent_kind"])
|
||
self.assertEqual("ops_job_event", activity_focus["source_focus_ref"]["kind"])
|
||
self.assertTrue(activity_focus["observation_only"])
|
||
self.assertEqual("ops_observability_contract", activity_focus["contract_navigation"]["primary_contract_key"])
|
||
self.assertIn("ops_stack_diagnosis_contract", activity_focus["contract_navigation"]["contract_keys"])
|
||
|
||
scene_log_observation = payload["scene_log_observation"]
|
||
self.assertEqual("disabled", scene_log_observation["status"])
|
||
self.assertEqual("mainland-worker-01", scene_log_observation["target_node_code"])
|
||
self.assertEqual("node_scene_log", scene_log_observation["focus_ref"]["kind"])
|
||
self.assertEqual(
|
||
"ops_observability_contract",
|
||
scene_log_observation["contract_navigation"]["primary_contract_key"],
|
||
)
|
||
|
||
runbook_entry = payload["entries"][2]
|
||
self.assertEqual("runbook_sequence", runbook_entry["kind"])
|
||
self.assertEqual("standard_path", runbook_entry["lane"])
|
||
self.assertEqual("runbook_sequence", runbook_entry["executor_kind"])
|
||
self.assertEqual("release_progression", runbook_entry["sequence_key"])
|
||
self.assertEqual("fix_managed_nodes", runbook_entry["primary_action_code"])
|
||
self.assertEqual("focus_release_hub", runbook_entry["secondary_action_code"])
|
||
self.assertIn("当前解析:补执行面接管", runbook_entry["detail_lines"][0])
|
||
self.assertEqual("release_hub", runbook_entry["focus_ref"]["kind"])
|
||
self.assertEqual("release_launchpad", runbook_entry["focus_ref"]["section"])
|
||
self.assertEqual("default_rollout_gate", runbook_entry["primary_focus_ref"]["section"])
|
||
self.assertEqual("release_detail", runbook_entry["secondary_focus_ref"]["section"])
|
||
self.assertEqual("default_rollout_gate", runbook_entry["primary_action_payload"]["focus_ref"]["section"])
|
||
self.assertEqual("release_detail", runbook_entry["secondary_action_payload"]["focus_ref"]["section"])
|
||
self.assertEqual("ops_driver_contract", runbook_entry["contract_navigation"]["primary_contract_key"])
|
||
self.assertIn("ops_playbook_contract", runbook_entry["contract_navigation"]["contract_keys"])
|
||
self.assertIn("release_hub_contract", runbook_entry["contract_navigation"]["contract_keys"])
|
||
|
||
@patch("app.services.ops_service.get_ops_go_live_summary")
|
||
@patch("app.services.ops_service.get_ops_runbook")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
def test_driver_feed_preserves_bootstrap_run_as_top_recommendation(
|
||
self,
|
||
mock_get_ops_overview,
|
||
mock_get_ops_runbook,
|
||
mock_get_ops_go_live_summary,
|
||
) -> None:
|
||
mock_get_ops_go_live_summary.return_value = {
|
||
"go_live_status": "attention",
|
||
"publish_ready": False,
|
||
"publish_status": "attention",
|
||
"publish_status_label": "可发布但建议先复核",
|
||
"publish_summary": "当前没有硬阻断,但仍有上线前关注项,建议先完成复核再正式发版。",
|
||
"operator_title": "先跑接入收口",
|
||
"next_step_action_code": "bootstrap_run",
|
||
"launchpad_recommended_target_node_code": "mainland-worker-01",
|
||
"launchpad_recommended_recovery_label": "跑接入收口",
|
||
"launchpad_recommended_recovery_summary": "mainland-worker-01 还缺接入收口,先执行 bootstrap。",
|
||
"launchpad_onboarding_bootstrap_pending_nodes": 1,
|
||
"launchpad_onboarding_acceptance_ready_nodes": 0,
|
||
"blocking_reasons": [],
|
||
"warnings": ["remote_agent_ready=1/2"],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {
|
||
"key": "handover-gap-bootstrap_run",
|
||
"priority": "先补接管缺口",
|
||
"title": "先补接管缺口",
|
||
"summary": "Launchpad 已明确收敛到首台节点接入收口。",
|
||
"reason": "mainland-worker-01 还缺接入收口,先执行 bootstrap。",
|
||
"level_label": "最高优先",
|
||
"tag_type": "warning",
|
||
"primary_label": "跑接入收口",
|
||
"secondary_label": "查看目标节点",
|
||
"primary_action_code": "bootstrap_run",
|
||
"secondary_action_code": "view_first_gap",
|
||
"node_codes": ["mainland-worker-01"],
|
||
"primary_node_codes": ["mainland-worker-01"],
|
||
"secondary_node_codes": ["mainland-worker-01"],
|
||
"primary_action_payload": {"node_code": "mainland-worker-01"},
|
||
"secondary_action_payload": {},
|
||
"focus_ref": {"kind": "managed_node", "node_code": "mainland-worker-01"},
|
||
"primary_focus_ref": {"kind": "managed_node", "node_code": "mainland-worker-01"},
|
||
"secondary_focus_ref": {"kind": "managed_node", "node_code": "mainland-worker-01"},
|
||
},
|
||
"driver_recommendations": [],
|
||
"activity_stream": {"items": []},
|
||
"execution_scene": {},
|
||
}
|
||
mock_get_ops_runbook.return_value = {"control_sequences": []}
|
||
|
||
payload = get_ops_driver_feed()
|
||
|
||
self.assertEqual("priority:handover-gap-bootstrap_run", payload["top_recommendation"]["key"])
|
||
self.assertEqual("attention", payload["summary"]["publish_status"])
|
||
self.assertEqual("可发布但建议先复核", payload["summary"]["publish_status_label"])
|
||
self.assertEqual("mainland-worker-01", payload["summary"]["launchpad_recommended_target_node_code"])
|
||
self.assertEqual("跑接入收口", payload["summary"]["launchpad_recommended_recovery_label"])
|
||
self.assertEqual(1, payload["summary"]["launchpad_onboarding_bootstrap_pending_nodes"])
|
||
self.assertEqual(0, payload["summary"]["launchpad_onboarding_acceptance_ready_nodes"])
|
||
self.assertEqual("attention", payload["summary"]["launch_status"])
|
||
self.assertFalse(payload["summary"]["launch_ready"])
|
||
self.assertEqual("bootstrap_run", payload["top_recommendation"]["primary_action_code"])
|
||
self.assertEqual("mainland-worker-01", payload["top_recommendation"]["primary_node_codes"][0])
|
||
self.assertIn("ops_agent_protocol", payload["top_recommendation"]["contract_navigation"]["contract_keys"])
|
||
self.assertEqual("ops_driver_contract", payload["top_recommendation"]["contract_navigation"]["primary_contract_key"])
|
||
self.assertEqual(0, payload["automation_coverage"]["safe_auto_total"])
|
||
self.assertEqual(1, payload["automation_coverage"]["mixed_total"])
|
||
self.assertEqual(0, payload["automation_coverage"]["preview_only_total"])
|
||
self.assertEqual(1, payload["automation_coverage"]["backend_handled_total"])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|