209 lines
7.5 KiB
Python
209 lines
7.5 KiB
Python
from types import SimpleNamespace
|
||
from unittest.mock import AsyncMock
|
||
|
||
import pytest
|
||
|
||
from src.services.approval_execution import ApprovalExecutionService
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_playbook_authoring_record_only_approval_does_not_resolve_incident(monkeypatch):
|
||
approval = SimpleNamespace(
|
||
id="approval-playbook-authoring-1",
|
||
action="PLAYBOOK_AUTHORING_RECORD_ONLY: promote diagnostic playbook",
|
||
incident_id="INC-TEST-PLAYBOOK",
|
||
metadata={
|
||
"approval_kind": "adr100_playbook_authoring",
|
||
"execution_kind": "playbook_authoring_record_only",
|
||
"execution_authorized": False,
|
||
"work_item_id": "verification:INC-TEST-PLAYBOOK:are-1",
|
||
"playbook_id": "PB-1",
|
||
},
|
||
)
|
||
incident_service = SimpleNamespace(resolve_incident=AsyncMock())
|
||
update_execution_status = AsyncMock()
|
||
timeline_add_event = AsyncMock()
|
||
alert_completed = AsyncMock(return_value=None)
|
||
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.get_approval_service",
|
||
lambda: SimpleNamespace(update_execution_status=update_execution_status),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.get_timeline_service",
|
||
lambda: SimpleNamespace(add_event=timeline_add_event),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.parse_operation_from_action",
|
||
lambda _: SimpleNamespace(
|
||
operation_type=None, resource_name=None, namespace=None
|
||
),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.incident_service.get_incident_service",
|
||
lambda: incident_service,
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._push_execution_result_to_alert",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_aol_completed",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_alert_execution_started",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_alert_execution_completed",
|
||
alert_completed,
|
||
)
|
||
|
||
result = await ApprovalExecutionService().execute_approved_action(approval)
|
||
|
||
assert result is True
|
||
update_execution_status.assert_awaited_once_with(
|
||
approval.id,
|
||
success=True,
|
||
execution_kind="playbook_authoring_record_only",
|
||
repair_executed=False,
|
||
repair_attempted=False,
|
||
)
|
||
assert "no runtime repair" in timeline_add_event.await_args.kwargs["title"]
|
||
assert alert_completed.await_args.kwargs["execution_kind"] == (
|
||
"playbook_authoring_record_only"
|
||
)
|
||
incident_service.resolve_incident.assert_not_awaited()
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_no_action_execution_resolves_incident_once(monkeypatch):
|
||
# Arrange
|
||
approval = SimpleNamespace(
|
||
id="approval-noaction-1",
|
||
action="NO_ACTION: 先做觀察",
|
||
incident_id="INC-TEST-001",
|
||
)
|
||
incident_service = SimpleNamespace(resolve_incident=AsyncMock())
|
||
update_execution_status = AsyncMock()
|
||
timeline_add_event = AsyncMock()
|
||
alert_completed = AsyncMock(return_value=None)
|
||
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.get_approval_service",
|
||
lambda: SimpleNamespace(update_execution_status=update_execution_status),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.get_timeline_service",
|
||
lambda: SimpleNamespace(add_event=timeline_add_event),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.parse_operation_from_action",
|
||
lambda _: SimpleNamespace(
|
||
operation_type=None, resource_name=None, namespace=None
|
||
),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.incident_service.get_incident_service",
|
||
lambda: incident_service,
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._push_execution_result_to_alert",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_aol_completed",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_alert_execution_started",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_alert_execution_completed",
|
||
alert_completed,
|
||
)
|
||
|
||
# Act
|
||
result = await ApprovalExecutionService().execute_approved_action(approval)
|
||
|
||
# Assert
|
||
assert result is True
|
||
update_execution_status.assert_awaited_once_with(
|
||
approval.id,
|
||
success=True,
|
||
execution_kind="no_action",
|
||
repair_executed=False,
|
||
repair_attempted=False,
|
||
)
|
||
assert "未執行修復" in timeline_add_event.await_args.kwargs["title"]
|
||
assert alert_completed.await_args.kwargs["execution_kind"] == "no_action"
|
||
assert alert_completed.await_args.kwargs["output"]["repair_executed"] is False
|
||
incident_service.resolve_incident.assert_awaited_once_with("INC-TEST-001")
|
||
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_no_action_execution_returns_true_when_resolve_raises(monkeypatch):
|
||
"""resolve_incident 拋錯時,NO_ACTION 仍須 return True。
|
||
|
||
契約:NO_ACTION 是「純觀察類」成功完成(line 207-208 註解明說避免污染
|
||
auto_execute KPI)。resolve 失敗只該 warning log,不該讓 result 退化成 False。
|
||
"""
|
||
approval = SimpleNamespace(
|
||
id="approval-noaction-2",
|
||
action="NO_ACTION: 觀察",
|
||
incident_id="INC-TEST-002",
|
||
)
|
||
incident_service = SimpleNamespace(
|
||
resolve_incident=AsyncMock(side_effect=RuntimeError("redis down"))
|
||
)
|
||
update_execution_status = AsyncMock()
|
||
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.get_approval_service",
|
||
lambda: SimpleNamespace(update_execution_status=update_execution_status),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.get_timeline_service",
|
||
lambda: SimpleNamespace(add_event=AsyncMock()),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.parse_operation_from_action",
|
||
lambda _: SimpleNamespace(
|
||
operation_type=None, resource_name=None, namespace=None
|
||
),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.incident_service.get_incident_service",
|
||
lambda: incident_service,
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._push_execution_result_to_alert",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_aol_completed",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_alert_execution_started",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
monkeypatch.setattr(
|
||
"src.services.approval_execution.ApprovalExecutionService._log_alert_execution_completed",
|
||
AsyncMock(return_value=None),
|
||
)
|
||
|
||
result = await ApprovalExecutionService().execute_approved_action(approval)
|
||
|
||
assert result is True
|
||
update_execution_status.assert_awaited_once_with(
|
||
approval.id,
|
||
success=True,
|
||
execution_kind="no_action",
|
||
repair_executed=False,
|
||
repair_attempted=False,
|
||
)
|
||
incident_service.resolve_incident.assert_awaited_once_with("INC-TEST-002")
|