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")