972 lines
42 KiB
Python
972 lines
42 KiB
Python
import unittest
|
||
from unittest.mock import patch
|
||
|
||
from app.services.ops_service import get_ops_stack_diagnosis
|
||
|
||
|
||
class OpsServiceStackDiagnosisTests(unittest.TestCase):
|
||
@patch("app.services.ops_service.get_runtime_build_info")
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_prioritizes_api_restart_when_runtime_build_schema_is_stale(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
mock_get_runtime_build_info,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [{"key": "ops_stack_diagnosis_contract"}],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "ready",
|
||
"headline": "当前总检可用。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {
|
||
"primary_action_code": "fix_managed_nodes",
|
||
"title": "先补节点接管",
|
||
"focus_ref": {"kind": "managed_node", "node_code": "mainland-worker-01"},
|
||
},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {"nodes": [], "summary": {}}
|
||
mock_get_release_launchpad.return_value = {"launchpad_status": {}, "latest_release": {}}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
mock_get_runtime_build_info.return_value = {
|
||
"source": "git",
|
||
"commit_sha": "246838ae4c07",
|
||
"repository_capabilities": {
|
||
"supports_install_command_block": True,
|
||
"supports_multi_layout_bootstrap": True,
|
||
},
|
||
"route_surface": {
|
||
"mode": "registered",
|
||
"registered_paths_total": 24,
|
||
"surface_complete": True,
|
||
"missing_keys": [],
|
||
"expected_paths": {
|
||
"ops_contracts": "/api/v1/ops/contracts",
|
||
"ops_stack_diagnosis": "/api/v1/ops/stack-diagnosis",
|
||
},
|
||
},
|
||
}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
issue = next(item for item in payload["diagnosis"]["issues"] if item["code"] == "runtime_build_schema_stale")
|
||
self.assertEqual("api-restart", issue["action_code"])
|
||
self.assertEqual("api-restart", payload["diagnosis"]["next_step"]["action_code"])
|
||
self.assertEqual("runtime_recovery", payload["diagnosis"]["operator_decision"]["lane"])
|
||
self.assertEqual("api-restart", payload["diagnosis"]["operator_decision"]["primary_command_key"])
|
||
self.assertEqual(
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover http://127.0.0.1:8100",
|
||
payload["diagnosis"]["next_actions"]["primary_command"],
|
||
)
|
||
self.assertEqual(
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover http://127.0.0.1:8100",
|
||
payload["diagnosis"]["recommended_commands"]["api-restart"],
|
||
)
|
||
self.assertEqual(
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover http://127.0.0.1:8100",
|
||
payload["diagnosis"]["recommended_commands"]["runtime-refresh-recover"],
|
||
)
|
||
self.assertEqual(
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh stack-diagnosis http://127.0.0.1:8100 summary",
|
||
payload["diagnosis"]["recommended_commands"]["stack-diagnosis"],
|
||
)
|
||
self.assertEqual(
|
||
payload["diagnosis"]["recommended_commands"]["stack-diagnosis"],
|
||
payload["diagnosis"]["recommended_commands"]["stack-check"],
|
||
)
|
||
self.assertTrue(payload["runtime_build_info"]["runtime_schema_stale"])
|
||
self.assertFalse(payload["runtime_build_info"]["route_surface_declares_bootstrap_plan"])
|
||
self.assertTrue(
|
||
any("运行中 API 还是旧 schema" in hint for hint in payload["diagnosis"]["operator_hints"])
|
||
)
|
||
|
||
@patch("app.services.ops_service.get_runtime_build_info")
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_surfaces_runtime_route_surface_gap(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
mock_get_runtime_build_info,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "ready",
|
||
"headline": "当前总检可用。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {"nodes": [], "summary": {}}
|
||
mock_get_release_launchpad.return_value = {"launchpad_status": {}, "latest_release": {}}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
mock_get_runtime_build_info.return_value = {
|
||
"source": "git",
|
||
"commit_sha": "bd6bcb2",
|
||
"route_surface": {
|
||
"mode": "registered",
|
||
"registered_paths_total": 12,
|
||
"surface_complete": False,
|
||
"missing_keys": [
|
||
"ops_node_onboarding_acceptance_execute",
|
||
"ops_node_onboarding_acceptance_preview",
|
||
],
|
||
"missing_paths": [
|
||
"/api/v1/ops/nodes/{node_code}/onboarding/acceptance/execute",
|
||
"/api/v1/ops/nodes/{node_code}/onboarding/acceptance/preview",
|
||
],
|
||
},
|
||
}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
issue = next(
|
||
item for item in payload["diagnosis"]["issues"] if item["code"] == "runtime_route_surface_incomplete"
|
||
)
|
||
self.assertEqual("api-restart", issue["action_code"])
|
||
self.assertIn("ops_node_onboarding_acceptance_execute", issue["detail"])
|
||
self.assertIn(
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh runtime-refresh-recover http://127.0.0.1:8100",
|
||
issue["commands"],
|
||
)
|
||
self.assertFalse(payload["runtime_build_info"]["route_surface"]["surface_complete"])
|
||
self.assertIn(
|
||
"ops_node_onboarding_acceptance_preview",
|
||
payload["runtime_build_info"]["route_surface_missing_keys"],
|
||
)
|
||
self.assertTrue(
|
||
any("API 路由面还不完整" in hint for hint in payload["diagnosis"]["operator_hints"])
|
||
)
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_emits_operator_decision_for_scene_log_gap(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "attention",
|
||
"headline": "参与节点样本尚未形成。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {
|
||
"participating_nodes": [{"node_code": "mainland-worker-01", "status": "busy"}],
|
||
"participation_summary": {
|
||
"dispatch_active_node_codes": ["mainland-worker-01"],
|
||
"recent_only_node_codes": [],
|
||
},
|
||
"log_sync": {
|
||
"enabled": True,
|
||
"mode": "key",
|
||
"line_count": 0,
|
||
"source_node_count": 0,
|
||
"covered_participating_node_count": 0,
|
||
"participating_node_count": 1,
|
||
"missing_participating_nodes": ["mainland-worker-01"],
|
||
},
|
||
},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {"nodes": [], "summary": {}}
|
||
mock_get_release_launchpad.return_value = {"launchpad_status": {}, "latest_release": {}}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
decision = payload["diagnosis"]["operator_decision"]
|
||
self.assertEqual("observability", decision["lane"])
|
||
self.assertEqual("scene-node-log", decision["primary_command_key"])
|
||
self.assertEqual("mainland-worker-01", decision["next_focus"]["node_code"])
|
||
self.assertIn("scene-node-log", payload["diagnosis"]["recommended_commands"])
|
||
self.assertIn(
|
||
"scene-node-log http://127.0.0.1:8100 mainland-worker-01 120 key",
|
||
payload["diagnosis"]["next_actions"]["primary_command"],
|
||
)
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.get_managed_node_handover")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_emits_operator_decision_for_managed_node_gap(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_managed_node_handover,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "attention",
|
||
"headline": "存在接管缺口。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {
|
||
"nodes": [
|
||
{
|
||
"node_code": "mainland-worker-01",
|
||
"agent_state": "pending_bootstrap",
|
||
"remote_access_state": "ssh_ready",
|
||
"delivery_queue_state": "healthy",
|
||
"participation_state": "standby",
|
||
}
|
||
],
|
||
"summary": {
|
||
"managed_enabled": 1,
|
||
"remote_access_ready": 0,
|
||
},
|
||
}
|
||
mock_get_managed_node_handover.return_value = {
|
||
"node_code": "mainland-worker-01",
|
||
"summary": "等待执行 bootstrap。",
|
||
"stage": {"code": "pending_bootstrap", "label": "待执行接入", "next_step": {"code": "execute_bootstrap_plan"}},
|
||
}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {
|
||
"recommended_action_code": "bootstrap_run",
|
||
"focus_ref": {"kind": "release_launchpad", "section": "release_launchpad"},
|
||
"recommended_target_node_code": "mainland-worker-01",
|
||
},
|
||
"worker_rollout_preview": {
|
||
"release_gate": {
|
||
"operational_readiness": {
|
||
"rows": [
|
||
{
|
||
"node_code": "mainland-worker-01",
|
||
"recovery_action": "bootstrap_run",
|
||
"recovery_summary": "mainland-worker-01 还缺接入收口,先执行 bootstrap。",
|
||
"focus_ref": {"kind": "managed_node", "node_code": "mainland-worker-01"},
|
||
}
|
||
]
|
||
}
|
||
}
|
||
},
|
||
"latest_release": {},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
decision = payload["diagnosis"]["operator_decision"]
|
||
self.assertEqual("node_handover", decision["lane"])
|
||
self.assertEqual("bootstrap_run", decision["primary_command_key"])
|
||
self.assertEqual("managed-node-bootstrap-plan", decision["secondary_command_key"])
|
||
self.assertIn("bootstrap_run", payload["diagnosis"]["recommended_commands"])
|
||
self.assertIn("managed-node-handover", payload["diagnosis"]["recommended_commands"])
|
||
self.assertIn("managed-node-bootstrap-plan", payload["diagnosis"]["recommended_commands"])
|
||
self.assertIn(
|
||
"node-bootstrap-run http://127.0.0.1:8100 mainland-worker-01 cli",
|
||
payload["diagnosis"]["next_actions"]["primary_command"],
|
||
)
|
||
self.assertTrue(
|
||
any("当前第一动作已经收敛为 接入收口:mainland-worker-01" in hint for hint in payload["diagnosis"]["operator_hints"])
|
||
)
|
||
|
||
@patch("app.services.ops_service.get_runtime_build_info")
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.get_managed_node_handover")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_promotes_first_handover_gap_when_launchpad_is_still_generic(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_managed_node_handover,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
mock_get_runtime_build_info,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [{"key": "ops_stack_diagnosis_contract"}],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "ready",
|
||
"headline": "当前总检可用。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {
|
||
"primary_action_code": "handover_first_gap",
|
||
"title": "先补接管缺口",
|
||
"reason": "overview 只能先定位到接管缺口。",
|
||
"focus_ref": {"kind": "managed_node", "node_code": "mainland-worker-01"},
|
||
},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {
|
||
"nodes": [
|
||
{
|
||
"node_code": "mainland-worker-01",
|
||
"agent_state": "pending_bootstrap",
|
||
"remote_access_state": "agent_pending",
|
||
"delivery_queue_state": "healthy",
|
||
"participation_state": "standby",
|
||
}
|
||
],
|
||
"summary": {
|
||
"managed_enabled": 1,
|
||
"remote_access_ready": 0,
|
||
},
|
||
}
|
||
mock_get_managed_node_handover.return_value = {
|
||
"node_code": "mainland-worker-01",
|
||
"summary": "当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap 并等待 register / heartbeat 建立。",
|
||
"stage": {
|
||
"code": "pending_bootstrap",
|
||
"label": "待执行接入",
|
||
"next_step": {
|
||
"code": "execute_bootstrap_plan",
|
||
"label": "去目标机执行接入片段",
|
||
},
|
||
},
|
||
}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {
|
||
"recommended_action_code": "fix_managed_nodes",
|
||
"status_label": "待补执行面",
|
||
"focus_ref": {"kind": "release_launchpad", "section": "release_launchpad"},
|
||
},
|
||
"latest_release": {},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
mock_get_runtime_build_info.return_value = {
|
||
"route_surface": {"surface_complete": True, "missing_keys": []},
|
||
"repository_capabilities": {},
|
||
}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
self.assertEqual("bootstrap_run", payload["diagnosis"]["next_step"]["action_code"])
|
||
self.assertEqual(
|
||
"mainland-worker-01",
|
||
payload["diagnosis"]["launchpad_recommended_target_node_code"],
|
||
)
|
||
self.assertEqual(
|
||
"签发接入工单",
|
||
payload["diagnosis"]["launchpad_recommended_recovery_label"],
|
||
)
|
||
self.assertIn(
|
||
"onboarding.bootstrap",
|
||
payload["diagnosis"]["launchpad_recommended_recovery_summary"],
|
||
)
|
||
self.assertEqual("bootstrap_run", payload["diagnosis"]["operator_decision"]["primary_command_key"])
|
||
self.assertEqual(
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh driver-resolve http://127.0.0.1:8100 bootstrap_run",
|
||
payload["diagnosis"]["next_actions"]["primary_command"],
|
||
)
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_emits_operator_decision_for_release_launchpad_attention(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "attention",
|
||
"headline": "发布门禁待处理。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {
|
||
"default_rollout_gate": {
|
||
"status": "blocked",
|
||
"summary_text": "还有节点未满足发布门禁。",
|
||
"focus_ref": {"kind": "release_hub", "section": "release_launchpad", "mode": "worker"},
|
||
}
|
||
},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {"nodes": [], "summary": {}}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {
|
||
"status": "attention",
|
||
"status_label": "待处理",
|
||
"recommended_action_code": "review_smart_rollout_preview",
|
||
"focus_ref": {"kind": "release_hub", "section": "release_launchpad", "mode": "worker"},
|
||
},
|
||
"latest_release": {"release_version": "release-20260418"},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
decision = payload["diagnosis"]["operator_decision"]
|
||
self.assertEqual("release", decision["lane"])
|
||
self.assertEqual("release-launchpad-worker", decision["primary_command_key"])
|
||
self.assertEqual("review_smart_rollout_preview", decision["secondary_command_key"])
|
||
self.assertIn("release-launchpad-worker", payload["diagnosis"]["recommended_commands"])
|
||
self.assertIn(
|
||
"release-launchpad http://127.0.0.1:8100 worker",
|
||
payload["diagnosis"]["next_actions"]["primary_command"],
|
||
)
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_prefers_node_recovery_when_launchpad_points_to_bootstrap_gap(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "attention",
|
||
"headline": "发布门禁待处理。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {
|
||
"default_rollout_gate": {
|
||
"status": "attention",
|
||
"summary_text": "当前还有节点未完成接入。",
|
||
"focus_ref": {"kind": "release_hub", "section": "release_launchpad", "mode": "worker"},
|
||
}
|
||
},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {"nodes": [], "summary": {}}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {
|
||
"status": "attention",
|
||
"status_label": "待补执行面",
|
||
"recommended_action_code": "bootstrap_run",
|
||
"recommended_target_node_code": "mainland-worker-02",
|
||
"recommended_recovery_label": "待执行接入",
|
||
"recommended_recovery_summary": "当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap。",
|
||
"onboarding_bootstrap_pending_nodes": 1,
|
||
"onboarding_acceptance_ready_nodes": 0,
|
||
"focus_ref": {"kind": "release_hub", "section": "release_launchpad", "mode": "worker"},
|
||
},
|
||
"latest_release": {"release_version": "release-20260418"},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
decision = payload["diagnosis"]["operator_decision"]
|
||
self.assertEqual("node_handover", decision["lane"])
|
||
self.assertEqual("bootstrap_run", decision["primary_command_key"])
|
||
self.assertEqual("focus-release-hub", decision["secondary_command_key"])
|
||
self.assertEqual("mainland-worker-02", payload["diagnosis"]["launchpad_recommended_target_node_code"])
|
||
self.assertEqual("待执行接入", payload["diagnosis"]["launchpad_recommended_recovery_label"])
|
||
self.assertEqual(1, payload["diagnosis"]["launchpad_onboarding_bootstrap_pending_nodes"])
|
||
self.assertEqual(0, payload["diagnosis"]["launchpad_onboarding_acceptance_ready_nodes"])
|
||
self.assertEqual(
|
||
"当前节点仍处于接入阶段,建议先签发 onboarding.bootstrap。",
|
||
decision["summary"],
|
||
)
|
||
self.assertEqual("mainland-worker-02", decision["next_focus"]["node_code"])
|
||
self.assertIn("node-bootstrap-run", payload["diagnosis"]["next_actions"]["primary_command"])
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.get_managed_node_handover")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_includes_first_handover_gap_details(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_managed_node_handover,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "attention",
|
||
"headline": "存在接管缺口。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {
|
||
"nodes": [
|
||
{
|
||
"node_code": "mainland-worker-01",
|
||
"agent_state": "pending_bootstrap",
|
||
"remote_access_state": "ssh_ready",
|
||
"delivery_queue_state": "healthy",
|
||
"participation_state": "standby",
|
||
}
|
||
],
|
||
"summary": {
|
||
"managed_enabled": 1,
|
||
"remote_access_ready": 0,
|
||
"status_counts": {"pending_bootstrap": 1},
|
||
"remote_access_state_counts": {"ssh_ready": 1},
|
||
"delivery_queue_state_counts": {"healthy": 1},
|
||
},
|
||
}
|
||
mock_get_managed_node_handover.return_value = {
|
||
"node_code": "mainland-worker-01",
|
||
"summary": "已签发有效 Token,但目标节点尚未执行 bootstrap 片段。",
|
||
"stage": {
|
||
"code": "pending_bootstrap",
|
||
"label": "待执行接入",
|
||
"next_step": {
|
||
"code": "execute_bootstrap_plan",
|
||
"label": "去目标机执行接入片段",
|
||
},
|
||
},
|
||
}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {
|
||
"recommended_action_code": "fix_managed_nodes",
|
||
"focus_ref": {"kind": "release_launchpad"},
|
||
},
|
||
"latest_release": {},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
self.assertEqual(
|
||
"mainland-worker-01",
|
||
payload["managed_nodes"]["first_handover_gap_node_code"],
|
||
)
|
||
self.assertEqual(
|
||
"pending_bootstrap",
|
||
payload["managed_nodes"]["first_handover_gap"]["stage"]["code"],
|
||
)
|
||
issue = next(
|
||
item for item in payload["diagnosis"]["issues"] if item["code"] == "managed_nodes_agent_pending"
|
||
)
|
||
self.assertIn("mainland-worker-01", issue["detail"])
|
||
self.assertTrue(
|
||
any("node-handover http://127.0.0.1:8100 mainland-worker-01" in command for command in issue["commands"])
|
||
)
|
||
self.assertIn(
|
||
"bash domain-api/deploy/multi-region/drive_ops_center.sh node-recover http://127.0.0.1:8100 mainland-worker-01",
|
||
payload["diagnosis"]["recommended_commands"]["managed-node-recover"],
|
||
)
|
||
self.assertTrue(
|
||
any("node-bootstrap-plan http://127.0.0.1:8100 mainland-worker-01" in command for command in issue["commands"])
|
||
)
|
||
mock_get_managed_node_handover.assert_called_once()
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_keeps_focus_refs_for_problem_runs_and_top_items(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "ready",
|
||
"headline": "当前总检可用。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {
|
||
"nodes": [],
|
||
"summary": {},
|
||
}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {},
|
||
"latest_release": {},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {
|
||
"runs": [
|
||
{
|
||
"run_code": "pbr-worker-logs-001",
|
||
"status": "attention",
|
||
"status_label": "待关注",
|
||
"focus_step_key": "worker_logs",
|
||
"focus_summary": "Worker 日志步骤仍未收口。",
|
||
"focus_ref": {
|
||
"kind": "playbook_run",
|
||
"run_code": "pbr-worker-logs-001",
|
||
"focus_step_key": "worker_logs",
|
||
},
|
||
}
|
||
]
|
||
}
|
||
mock_get_ops_activity_stream.return_value = {
|
||
"items": [
|
||
{
|
||
"kind": "ops_job",
|
||
"status": "running",
|
||
"summary_text": "远端 Agent 已回传关键日志样本。",
|
||
"occurred_at": "2026-04-18 09:22:00",
|
||
"focus_ref": {
|
||
"kind": "ops_job",
|
||
"job_id": 71,
|
||
"job_code": "ops-agent-71",
|
||
},
|
||
"source_focus_ref": {
|
||
"kind": "ops_job_event",
|
||
"job_id": 71,
|
||
"event_key": "job-event:71",
|
||
},
|
||
"ui_intent": {
|
||
"kind": "job_events",
|
||
"job_id": 71,
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
self.assertEqual(
|
||
"ops_stack_diagnosis_contract",
|
||
payload["diagnosis"]["contract_navigation"]["primary_contract_key"],
|
||
)
|
||
self.assertIn("ops_driver_contract", payload["diagnosis"]["contract_navigation"]["contract_keys"])
|
||
self.assertEqual("playbook_run", payload["playbook_runs"]["problem_runs"][0]["focus_ref"]["kind"])
|
||
self.assertEqual("worker_logs", payload["playbook_runs"]["problem_runs"][0]["focus_ref"]["focus_step_key"])
|
||
top_item = payload["activity_stream"]["top_items"][0]
|
||
self.assertEqual("ops_job", top_item["focus_ref"]["kind"])
|
||
self.assertEqual("ops_job_event", top_item["source_focus_ref"]["kind"])
|
||
self.assertEqual("job_events", top_item["ui_intent_kind"])
|
||
self.assertEqual("2026-04-18 09:22:00", top_item["occurred_at"])
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_adds_node_scene_log_commands_for_log_sample_gap(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "attention",
|
||
"headline": "参与节点样本尚未形成。",
|
||
"next_actions": [
|
||
{
|
||
"label": "看参与节点日志",
|
||
"action_code": "open_worker_logs_participating",
|
||
"node_codes": ["mainland-worker-01"],
|
||
"payload": {},
|
||
}
|
||
],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {
|
||
"participating_nodes": [
|
||
{
|
||
"node_code": "mainland-worker-01",
|
||
"status": "busy",
|
||
}
|
||
],
|
||
"participation_summary": {
|
||
"dispatch_active_node_codes": ["mainland-worker-01"],
|
||
"recent_only_node_codes": [],
|
||
},
|
||
"log_sync": {
|
||
"enabled": True,
|
||
"mode": "key",
|
||
"line_count": 0,
|
||
"source_node_count": 0,
|
||
"covered_participating_node_count": 0,
|
||
"participating_node_count": 1,
|
||
"missing_participating_nodes": ["mainland-worker-01"],
|
||
},
|
||
},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {
|
||
"nodes": [],
|
||
"summary": {},
|
||
}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {},
|
||
"latest_release": {},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {"items": []}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
issue = next(
|
||
item for item in payload["diagnosis"]["issues"] if item["code"] == "remote_log_sync_waiting_sample"
|
||
)
|
||
self.assertEqual("open_worker_logs_participating", issue["action_code"])
|
||
self.assertEqual("node_scene_log", issue["focus_ref"]["kind"])
|
||
self.assertEqual("mainland-worker-01", issue["focus_ref"]["node_code"])
|
||
self.assertTrue(
|
||
any(
|
||
"scene-node-log http://127.0.0.1:8100 mainland-worker-01 120 key" in command
|
||
for command in issue["commands"]
|
||
)
|
||
)
|
||
self.assertEqual("node_scene_log", payload["diagnosis"]["next_step"]["focus_ref"]["kind"])
|
||
self.assertTrue(
|
||
any(
|
||
"scene-node-log http://127.0.0.1:8100 mainland-worker-01 120 key" in command
|
||
for command in payload["diagnosis"]["quick_commands"]
|
||
)
|
||
)
|
||
self.assertTrue(
|
||
any(
|
||
"driver-resolve http://127.0.0.1:8100 open_worker_logs_participating" in command
|
||
for command in issue["commands"]
|
||
)
|
||
)
|
||
|
||
@patch("app.services.ops_service.get_ops_activity_stream")
|
||
@patch("app.services.ops_service.get_recent_ops_playbook_runs")
|
||
@patch("app.services.ops_service.get_release_launchpad")
|
||
@patch("app.services.ops_service.list_managed_nodes_with_agent_state")
|
||
@patch("app.services.ops_service.get_ops_overview")
|
||
@patch("app.services.ops_service.get_ops_link_snapshot")
|
||
@patch("app.services.ops_service.get_ops_contract_registry")
|
||
def test_stack_diagnosis_surfaces_start_delivery_gap_from_activity_stream(
|
||
self,
|
||
mock_get_ops_contract_registry,
|
||
mock_get_ops_link_snapshot,
|
||
mock_get_ops_overview,
|
||
mock_list_managed_nodes_with_agent_state,
|
||
mock_get_release_launchpad,
|
||
mock_get_recent_ops_playbook_runs,
|
||
mock_get_ops_activity_stream,
|
||
) -> None:
|
||
mock_get_ops_contract_registry.return_value = {
|
||
"registry_version": "2026-04-18",
|
||
"schema_version": "v1",
|
||
"contracts": [],
|
||
}
|
||
mock_get_ops_link_snapshot.return_value = {
|
||
"status": "attention",
|
||
"headline": "最近有任务回执异常。",
|
||
"next_actions": [],
|
||
}
|
||
mock_get_ops_overview.return_value = {
|
||
"recommendation": {},
|
||
"execution_scene": {},
|
||
"log_sync": {},
|
||
"release_hub": {},
|
||
}
|
||
mock_list_managed_nodes_with_agent_state.return_value = {
|
||
"nodes": [],
|
||
"summary": {},
|
||
}
|
||
mock_get_release_launchpad.return_value = {
|
||
"launchpad_status": {},
|
||
"latest_release": {},
|
||
}
|
||
mock_get_recent_ops_playbook_runs.return_value = {"runs": []}
|
||
mock_get_ops_activity_stream.return_value = {
|
||
"items": [
|
||
{
|
||
"kind": "ops_job",
|
||
"status": "attention",
|
||
"job_status": "running",
|
||
"summary_text": "节点已接单执行,但开始回执未成功送达控制面。",
|
||
"occurred_at": "2026-04-18 10:22:00",
|
||
"job_code": "ops-agent-81",
|
||
"start_delivery_state": "failed_local",
|
||
"start_delivery_error": "temporary offline",
|
||
"focus_ref": {
|
||
"kind": "ops_job",
|
||
"job_id": 81,
|
||
"job_code": "ops-agent-81",
|
||
"target_node_code": "mainland-worker-01",
|
||
},
|
||
"source_focus_ref": {
|
||
"kind": "ops_job_event",
|
||
"job_id": 81,
|
||
"job_code": "ops-agent-81",
|
||
"event_key": "job-event:81",
|
||
"target_node_code": "mainland-worker-01",
|
||
},
|
||
"ui_intent": {
|
||
"kind": "job_events",
|
||
"job_id": 81,
|
||
},
|
||
}
|
||
]
|
||
}
|
||
|
||
payload = get_ops_stack_diagnosis(base_url="http://127.0.0.1:8100")
|
||
|
||
issue = next(
|
||
item for item in payload["diagnosis"]["issues"] if item["code"] == "ops_job_start_delivery_failed_local"
|
||
)
|
||
self.assertEqual("focus_latest_job_events", issue["action_code"])
|
||
self.assertEqual("ops_job_event", issue["focus_ref"]["kind"])
|
||
self.assertIn("temporary offline", issue["detail"])
|
||
decision = payload["diagnosis"]["operator_decision"]
|
||
self.assertEqual("ops_jobs", decision["lane"])
|
||
self.assertEqual("focus_latest_job_events", decision["primary_command_key"])
|
||
self.assertIn("driver-resolve http://127.0.0.1:8100 focus_latest_job_events", payload["diagnosis"]["next_actions"]["primary_command"])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|