"""Webhook AI decisions must route through the single-writer broker.""" from __future__ import annotations import inspect from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from src.api.v1 import webhooks from src.models.ai import ( AIBlastRadius, AIDataImpact, OpenClawDecision, SuggestedAction, ) from src.models.approval import RiskLevel from src.services.approval_execution import ApprovalExecutionService def _make_analysis( *, confidence: float = 0.90, kubectl_command: str = "kubectl rollout restart deployment/api -n prod", suggested_action: SuggestedAction = SuggestedAction.RESTART_DEPLOYMENT, ) -> OpenClawDecision: return OpenClawDecision( action_title="Restart deployment", kubectl_command=kubectl_command, description="Controlled restart candidate", risk_level="low", suggested_action=suggested_action, confidence=confidence, blast_radius=AIBlastRadius( affected_pods=1, estimated_downtime="~30s", related_services=[], data_impact=AIDataImpact.NONE, ), target_resource="deployment/api", affected_services=[], deviation_analysis="none", primary_responsibility="COLLAB", ) def _can_route(analysis: OpenClawDecision, risk_level: RiskLevel) -> bool: from src.services.action_parser import is_safe_kubectl_action non_actions = {"NO_ACTION", "INVESTIGATE", "OBSERVE"} action_name = analysis.suggested_action.value command = (analysis.kubectl_command or "").strip() return ( bool(command) and analysis.confidence >= 0.85 and risk_level != RiskLevel.CRITICAL and action_name not in non_actions and is_safe_kubectl_action(command) ) @pytest.mark.parametrize( ("analysis", "risk_level", "expected"), [ (_make_analysis(), RiskLevel.LOW, True), (_make_analysis(confidence=0.70), RiskLevel.LOW, False), (_make_analysis(confidence=0.85), RiskLevel.MEDIUM, True), (_make_analysis(), RiskLevel.CRITICAL, False), ( _make_analysis( kubectl_command="kubectl delete pods --all -n prod", ), RiskLevel.LOW, False, ), ( _make_analysis(suggested_action=SuggestedAction.NO_ACTION), RiskLevel.LOW, False, ), ], ) def test_cs1_controlled_queue_eligibility( analysis: OpenClawDecision, risk_level: RiskLevel, expected: bool, ) -> None: assert _can_route(analysis, risk_level) is expected def test_active_webhook_paths_do_not_call_direct_executor() -> None: receive_source = inspect.getsource(webhooks.receive_alert) alertmanager_source = inspect.getsource(webhooks._process_new_alert_background) router_source = inspect.getsource(webhooks._try_auto_repair_background) legacy_source = inspect.getsource( webhooks._legacy_try_auto_repair_background_disabled ) for source in (receive_source, alertmanager_source, router_source): assert "execute_approved_action" not in source assert "ApprovalExecutionService" not in source assert "execute_auto_repair(" not in source assert "_try_auto_repair_background" in receive_source assert "_try_auto_repair_background" in alertmanager_source assert "enqueue_ai_decision_ansible_candidate" in router_source assert "webhook_direct_auto_repair_superseded" in legacy_source @pytest.mark.asyncio async def test_webhook_router_writes_queue_receipt_without_side_effect( monkeypatch: pytest.MonkeyPatch, ) -> None: incident = SimpleNamespace(project_id="awoooi") incident_service = SimpleNamespace( get_from_working_memory=AsyncMock(return_value=incident) ) append = AsyncMock() async def queue(**_kwargs): return { "schema_version": "ai_decision_controlled_executor_handoff_v1", "status": "controlled_check_mode_queued", "automation_run_id": "00000000-0000-0000-0000-000000000101", "queued": True, "side_effect_performed": False, "single_writer_executor": "awoooi-ansible-executor-broker", "active_blockers": [], } monkeypatch.setattr(webhooks, "get_incident_service", lambda: incident_service) monkeypatch.setattr( "src.jobs.awooop_ansible_candidate_backfill_job.enqueue_ai_decision_ansible_candidate", queue, ) monkeypatch.setattr( "src.repositories.alert_operation_log_repository.get_alert_operation_log_repository", lambda: SimpleNamespace(append=append), ) handoff = await webhooks._try_auto_repair_background( incident_id="INC-QUEUE", approval_id="00000000-0000-0000-0000-000000000201", alert_type="MomoPostgresBackupFailed", target_resource="momo-postgres", namespace="awoooi-prod", risk_level="low", ) assert handoff["queued"] is True assert handoff["side_effect_performed"] is False append.assert_awaited_once() assert append.await_args.kwargs["context"]["side_effect_performed"] is False @pytest.mark.asyncio async def test_approval_executor_rejects_auto_approved_direct_caller() -> None: approval = SimpleNamespace( id="00000000-0000-0000-0000-000000000301", incident_id="INC-DIRECT-BLOCK", requested_by="auto_approve_rule_engine", ) with pytest.raises( RuntimeError, match="auto_approved_direct_execution_disabled_use_single_writer_broker", ): await ApprovalExecutionService().execute_approved_action(approval)