import asyncio import inspect from datetime import datetime from decimal import Decimal from types import SimpleNamespace from uuid import UUID import pytest from fastapi import HTTPException import src.services.platform_operator_service as platform_operator_service from src.api.v1.platform.operator_runs import ( AiRouteStatusResponse, ListApprovalsResponse, ListCallbackRepliesResponse, ListCicdEventsResponse, ListRunsResponse, ) from src.services.ollama_failover_manager import OllamaEndpoint, OllamaRoutingResult from src.services.ollama_health_monitor import HealthReport, HealthStatus from src.services.platform_operator_service import ( _RUN_CONTEXT_QUERY_CHUNK_SIZE, _ai_route_health_map, _ai_route_lane_state, _ai_route_policy_order, _ai_route_repair_evidence_item, _build_awooop_status_chain, _callback_reply_audit_summary_from_row, _callback_reply_event_item, _callback_reply_summary_matches_status, _cicd_duration_seconds, _cicd_event_item_from_row, _collect_run_incident_ids, _is_source_correlation_applied_link, _iter_run_context_batches, _legacy_mcp_timeline_status, _legacy_mcp_timeline_summary, _list_filter_context_limit, _outbound_timeline_status, _outbound_timeline_summary, _outbound_timeline_title, _recent_channel_event_item, _recent_event_source_summary, _remediation_summary_matches_incident_id, _remediation_summary_matches_status, _remediation_timeline_summary, _run_callback_reply_summary, _run_remediation_list_summary, _score_source_correlation_event, _source_event_correlation_context, _timeline_sort_key, _validate_ai_route_workload, _validate_callback_reply_action_filter, _validate_callback_reply_status_filter, _validate_cicd_stage_filter, _validate_cicd_status_filter, ) def test_outbound_timeline_title_labels_runbook_review() -> None: title = _outbound_timeline_title( "telegram", "approval_request", "📄 RUNBOOK REVIEW|待審核\nIncident:INC-1", ) assert title == "TELEGRAM:Runbook 待人工審核" def test_outbound_timeline_title_labels_governance_alert() -> None: title = _outbound_timeline_title( "telegram", "final", "⚠️ *AI 治理警報|知識庫劣化*", ) assert title == "TELEGRAM:AI 治理警報" def test_outbound_timeline_title_labels_cicd_status() -> None: title = _outbound_timeline_title( "telegram", "final", "✅ [AWOOOI CI/CD] | code-review\n📦 Code Review 完成・LOW", ) assert title == "TELEGRAM:CI/CD 狀態通知" def test_recent_event_source_summary_projects_telegram_callback_safely() -> None: row = SimpleNamespace( event_id=UUID("11111111-1111-4111-8111-111111111111"), project_id="awoooi", channel_type="telegram", provider_event_id="telegram_callback:503475699", channel_chat_id=None, run_id=None, content_type="callback_query", content_preview=( "Telegram callback_query received; action=approve; " "incident_id=INC-20260602-5734BE" ), is_duplicate=False, received_at=datetime(2026, 6, 4, 7, 16, 33), source_envelope={ "schema_version": "inbound_source_envelope_v1", "provider": "telegram", "stage": "received", "provider_event_id": "telegram_callback:503475699", "redaction_version": "audit_sink_v1", "extra": { "telegram_callback_query": { "callback_query_id_sha256": "q" * 64, "callback_data_sha256": "d" * 64, "callback_action": "approve", "callback_ref": "INC-20260602-5734BE", "incident_id": "INC-20260602-5734BE", "approval_id": None, "message_id": "30972", "user_id_sha256": "u" * 64, "username_present": True, } }, }, ) summary = _recent_event_source_summary(row) assert summary["schema_version"] == "awooop_recent_event_source_summary_v1" assert summary["provider"] == "telegram" assert summary["stage"] == "received" assert summary["source_ref_count"] == 0 assert summary["telegram_callback_query"] == { "action": "approve", "callback_ref": "INC-20260602-5734BE", "incident_id": "INC-20260602-5734BE", "approval_id": None, "message_id": "30972", "username_present": True, } dumped = str(summary) assert "sha256" not in dumped assert "callback_data" not in dumped assert "user_id" not in dumped def test_recent_channel_event_item_includes_content_type_and_source_summary() -> None: run_id = UUID("22222222-2222-4222-8222-222222222222") row = SimpleNamespace( event_id=UUID("33333333-3333-4333-8333-333333333333"), project_id="awoooi", channel_type="internal", provider_event_id="alertmanager:received:alert-1", channel_chat_id=None, run_id=run_id, content_type="text", content_preview="Alertmanager inbound received", is_duplicate=True, received_at=datetime(2026, 6, 4, 13, 15, 2), source_envelope={ "provider": "alertmanager", "stage": "received", "source_refs": { "alert_ids": ["alert-1"], "incident_ids": ["INC-20260603-9B2535"], }, "log_correlation": { "alertname": "DockerContainerUnhealthy", "severity": "warning", "namespace": "default", "target_resource": "bitan-pharmacy-bitan-1", "fingerprint": "be6a1821f6336fa44b5ec33855b9f23d", }, }, ) item = _recent_channel_event_item(row) assert item["run_id"] == run_id assert item["content_type"] == "text" assert item["source_summary"]["provider"] == "alertmanager" assert item["source_summary"]["stage"] == "received" assert item["source_summary"]["source_ref_count"] == 2 assert item["source_summary"]["log_correlation"]["alertname"] == ( "DockerContainerUnhealthy" ) def test_cicd_event_item_preserves_rollout_risk_summary() -> None: item = _cicd_event_item_from_row( { "id": "1da1af11-fd3e-4073-ac85-fd304dbd2dc3", "action_detail": "收到告警: CI_rollout_risk_pending", "created_at": datetime(2026, 5, 21, 11, 46, 33), "context": { "source": "alertmanager", "alert_id": "alert-20260521194633", "labels": { "alertname": "CI_rollout_risk_pending", "stage": "rollout-risk", "status": "pending", "severity": "warning", "commit": "8e68dc1e3595a2667831143f76794512bcb302be", "triggered_by": "wooo", "duration_seconds": "0", }, "annotations": { "summary": "AWOOOI 部署風險已恢復", "description": "public_health_argocd_wait_http=curl_error_28", "workflow_url": "http://192.168.0.110:3001/wooo/awoooi/actions/runs/2827", }, }, }, project_id="awoooi", ) assert item["stage"] == "rollout-risk" assert item["status"] == "pending" assert item["needs_attention"] is True assert item["summary"] == "AWOOOI 部署風險已恢復" assert "curl_error_28" in item["description"] assert item["commit_sha"].startswith("8e68dc1e") assert ListCicdEventsResponse(items=[item], total=1, limit=1).items[0].stage == ( "rollout-risk" ) def test_cicd_event_filter_validation_and_duration_safety() -> None: assert _validate_cicd_stage_filter("Rollout-Risk") == "rollout-risk" assert _validate_cicd_status_filter("PENDING") == "pending" assert _cicd_duration_seconds("-3") == 0 assert _cicd_duration_seconds("bad") == 0 with pytest.raises(HTTPException): _validate_cicd_stage_filter("rollout risk;drop") with pytest.raises(HTTPException): _validate_cicd_status_filter("ignored") def test_outbound_timeline_title_labels_auto_repair_handoff() -> None: title = _outbound_timeline_title( "telegram", "error", "🤖❌ HANDOFF REQUIRED|AI 自動修復失敗,已轉人工", ) assert title == "TELEGRAM:AI 自動修復失敗,已轉人工" def test_outbound_timeline_title_falls_back_to_human_label() -> None: title = _outbound_timeline_title("telegram", "interim", "正在調用 MCP 工具") assert title == "TELEGRAM:漸進式狀態回饋" def test_outbound_timeline_title_labels_callback_reply_fallback() -> None: callback_reply = { "status": "callback_reply_fallback_sent", "action": "history", "incident_id": "INC-20260513-79ED5E", "parse_mode": "plain_text", } title = _outbound_timeline_title( "telegram", "final", "事件歷史統計 INC-20260513-79ED5E", callback_reply, ) summary = _outbound_timeline_summary( content_preview="事件歷史統計 INC-20260513-79ED5E", send_error=None, callback_reply=callback_reply, ) assert title == "TELEGRAM:歷史回覆 fallback 已送出" assert _outbound_timeline_status("sent", callback_reply) == ( "callback_reply_fallback_sent" ) assert "callback=history" in summary assert "incident=INC-20260513-79ED5E" in summary assert "parse_mode=plain_text" in summary def test_outbound_timeline_title_labels_callback_reply_failure() -> None: callback_reply = { "status": "callback_reply_failed", "action": "detail", "incident_id": "INC-20260513-79ED5E", "error": "HTTP error: 400", } assert _outbound_timeline_title("telegram", "error", None, callback_reply) == ( "TELEGRAM:詳情回覆送出失敗" ) assert _outbound_timeline_status("failed", callback_reply) == "callback_reply_failed" assert "error=HTTP error: 400" in _outbound_timeline_summary( content_preview=None, send_error="HTTP error: 400", callback_reply=callback_reply, ) def test_collect_run_incident_ids_reads_source_refs_and_legacy_text() -> None: run = SimpleNamespace( trigger_ref="not-an-incident", error_detail=None, ) inbound_events = [ SimpleNamespace( source_envelope={ "source_refs": { "incident_ids": ["INC-20260514-F85F21", "INC-20260514-F85F21"], } }, content_preview="Alertmanager inbound converged", content_redacted=None, ) ] outbound_messages = [ SimpleNamespace( source_envelope={ "source_refs": { "incident_ids": ["INC-20260518-CB0001"], }, }, content_preview="詳情:INC-20260513-79ED5E", send_error=None, ) ] incident_ids = _collect_run_incident_ids( run=run, inbound_events=inbound_events, outbound_messages=outbound_messages, ) assert incident_ids == [ "INC-20260514-F85F21", "INC-20260518-CB0001", "INC-20260513-79ED5E", ] def test_run_callback_reply_summary_marks_latest_fallback() -> None: summary = _run_callback_reply_summary([ SimpleNamespace( source_envelope={ "callback_reply": { "status": "callback_reply_sent", "action": "detail", "incident_id": "INC-20260513-79ED5E", } }, sent_at=datetime(2026, 5, 18, 6, 1, 0), queued_at=datetime(2026, 5, 18, 6, 1, 0), provider_message_id="100", ), SimpleNamespace( source_envelope={ "callback_reply": { "status": "callback_reply_fallback_sent", "action": "history", "incident_id": "INC-20260513-79ED5E", } }, sent_at=datetime(2026, 5, 18, 6, 2, 0), queued_at=datetime(2026, 5, 18, 6, 2, 0), provider_message_id="101", ), ]) assert summary["status"] == "fallback_sent" assert summary["total"] == 2 assert summary["sent"] == 1 assert summary["fallback_sent"] == 1 assert summary["latest_action"] == "history" assert summary["latest_incident_id"] == "INC-20260513-79ED5E" assert summary["latest_provider_message_id"] == "101" assert summary["needs_human"] is False assert summary["capture_status"] == "not_captured" assert summary["capture_not_captured"] == 2 assert summary["latest_capture_missing"] == [ "awooop_status_chain", "km_stale_completion_summary", ] def test_run_callback_reply_summary_marks_failed_as_human_attention() -> None: summary = _run_callback_reply_summary([ SimpleNamespace( source_envelope={ "callback_reply": { "status": "callback_reply_failed", "action": "detail", "incident_id": "INC-20260513-79ED5E", } }, sent_at=None, queued_at=datetime(2026, 5, 18, 6, 3, 0), provider_message_id="telegram_callback_reply:failed", ) ]) assert summary["status"] == "failed" assert summary["failed"] == 1 assert summary["needs_human"] is True assert summary["capture_status"] == "not_captured" def test_run_callback_reply_summary_counts_capture_statuses() -> None: summary = _run_callback_reply_summary([ SimpleNamespace( source_envelope={ "callback_reply": { "status": "callback_reply_sent", "action": "detail", "incident_id": "INC-20260513-79ED5E", }, "awooop_status_chain": { "schema_version": "awooop_status_chain_callback_reply_snapshot_v1", }, "km_stale_completion_summary": { "schema_version": ( "km_stale_owner_review_callback_reply_snapshot_v1" ), }, }, sent_at=datetime(2026, 5, 18, 6, 3, 0), queued_at=datetime(2026, 5, 18, 6, 3, 0), provider_message_id="102", ), SimpleNamespace( source_envelope={ "callback_reply": { "status": "callback_reply_sent", "action": "history", "incident_id": "INC-20260513-79ED5E", }, "awooop_status_chain": { "schema_version": "awooop_status_chain_callback_reply_snapshot_v1", }, }, sent_at=datetime(2026, 5, 18, 6, 4, 0), queued_at=datetime(2026, 5, 18, 6, 4, 0), provider_message_id="103", ), ]) assert summary["capture_status"] == "partial" assert summary["capture_captured"] == 1 assert summary["capture_partial"] == 1 assert summary["capture_not_captured"] == 0 assert summary["latest_capture_status"] == "partial" assert summary["latest_capture_missing"] == ["km_stale_completion_summary"] def test_run_callback_reply_summary_marks_no_callback() -> None: summary = _run_callback_reply_summary([ SimpleNamespace( source_envelope={}, sent_at=datetime(2026, 5, 18, 6, 1, 0), queued_at=datetime(2026, 5, 18, 6, 1, 0), provider_message_id="100", ) ]) assert summary["status"] == "no_callback" assert summary["total"] == 0 assert summary["capture_status"] == "no_callback" def test_list_runs_response_preserves_callback_reply_summary() -> None: run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") response = ListRunsResponse.model_validate({ "runs": [ { "run_id": run_id, "project_id": "awoooi", "agent_id": "legacy-telegram-gateway", "state": "completed", "is_shadow": True, "cost_usd": Decimal("0.0000"), "step_count": 0, "created_at": datetime(2026, 5, 18, 7, 31, 37), "timeout_at": None, "remediation_summary": None, "callback_reply_summary": { "schema_version": "awooop_run_callback_reply_summary_v1", "status": "failed", "total": 1, "sent": 0, "fallback_sent": 0, "rescue_sent": 0, "failed": 1, "needs_human": True, "latest_status": "callback_reply_failed", "latest_action": "detail", "latest_incident_id": "INC-20260513-79ED5E", "latest_at": "2026-05-18T07:31:37", "latest_provider_message_id": "telegram_callback_reply:failed", "capture_status": "not_captured", "capture_captured": 0, "capture_partial": 0, "capture_not_captured": 1, "latest_capture_status": "not_captured", "latest_capture_missing": [ "awooop_status_chain", "km_stale_completion_summary", ], "latest_capture_next_action": ( "press_telegram_detail_or_history_after_rollout" ), }, } ], "total": 1, "page": 1, "per_page": 1, }) dumped = response.model_dump(mode="json") assert dumped["runs"][0]["callback_reply_summary"]["status"] == "failed" assert dumped["runs"][0]["callback_reply_summary"]["needs_human"] is True assert dumped["runs"][0]["callback_reply_summary"]["capture_status"] == ( "not_captured" ) def test_callback_reply_event_item_surfaces_run_link_and_human_flag() -> None: run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") message_id = UUID("56cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92") item = _callback_reply_event_item({ "message_id": message_id, "run_id": run_id, "project_id": "awoooi", "channel_type": "telegram", "message_type": "error", "send_status": "failed", "send_error": "HTTP error: 400", "provider_message_id": "telegram_callback_reply:failed", "queued_at": datetime(2026, 5, 18, 7, 31, 37), "sent_at": None, "triggered_by_state": "callback_reply", "content_preview": "無法取得歷史統計", "run_state": "completed", "agent_id": "legacy-telegram-gateway", "run_created_at": datetime(2026, 5, 18, 7, 30, 0), "callback_reply": { "status": "callback_reply_failed", "action": "history", "incident_id": "INC-20260513-79ED5E", "error": "HTTP error: 400", }, "persisted_awooop_status_chain": { "schema_version": "awooop_status_chain_callback_reply_snapshot_v1", "repair_state": "blocked_manual_required", "needs_human": True, "next_step": "manual_investigation", }, "persisted_km_stale_completion_summary": { "schema_version": "km_stale_owner_review_callback_reply_snapshot_v1", "status": "no_related_owner_review", "ready_count": 4, "triage": { "flow_stage": "callback_observed_owner_review_link_missing", "ai_lead_agent": "Hermes", }, }, }) assert item["status"] == "failed" assert item["needs_human"] is True assert item["action"] == "history" assert item["incident_id"] == "INC-20260513-79ED5E" assert item["event_at"] == datetime(2026, 5, 18, 7, 31, 37) assert item["run_detail_href"] == ( "/awooop/runs/5c0306e0-591a-5445-9a33-80f499426b38?project_id=awoooi" ) assert item["persisted_km_stale_completion_summary"]["ready_count"] == 4 assert item["persisted_km_stale_completion_summary"]["triage"]["ai_lead_agent"] == ( "Hermes" ) assert item["persisted_awooop_status_chain"]["repair_state"] == ( "blocked_manual_required" ) assert item["evidence_capture_status"]["status"] == "captured" assert item["evidence_capture_status"]["captured"] == [ "awooop_status_chain", "km_stale_completion_summary", ] assert item["evidence_capture_status"]["missing"] == [] assert item["evidence_capture_status"]["next_action"] == "none" def test_callback_reply_event_item_marks_legacy_snapshot_missing() -> None: run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") message_id = UUID("56cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92") item = _callback_reply_event_item({ "message_id": message_id, "run_id": run_id, "project_id": "awoooi", "channel_type": "telegram", "message_type": "final", "send_status": "sent", "send_error": None, "provider_message_id": "123", "queued_at": datetime(2026, 5, 18, 7, 31, 37), "sent_at": datetime(2026, 5, 18, 7, 31, 38), "triggered_by_state": "callback_reply", "content_preview": "事件詳情", "run_state": "completed", "agent_id": "legacy-telegram-gateway", "run_created_at": datetime(2026, 5, 18, 7, 30, 0), "callback_reply": { "status": "callback_reply_sent", "action": "detail", "incident_id": "INC-20260513-79ED5E", }, "persisted_awooop_status_chain": None, "persisted_km_stale_completion_summary": None, }) capture_status = item["evidence_capture_status"] assert capture_status["schema_version"] == "callback_evidence_capture_status_v1" assert capture_status["status"] == "not_captured" assert capture_status["reason"] == "legacy_callback_before_snapshot_rollout" assert capture_status["missing"] == [ "awooop_status_chain", "km_stale_completion_summary", ] assert capture_status["captured"] == [] assert capture_status["next_action"] == ( "press_telegram_detail_or_history_after_rollout" ) def test_list_callback_replies_response_preserves_callback_evidence() -> None: run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") message_id = UUID("56cdb6ad-46a4-48f5-9d3b-b1ac9c0b2e92") response = ListCallbackRepliesResponse.model_validate({ "items": [ { "message_id": message_id, "run_id": run_id, "project_id": "awoooi", "status": "fallback_sent", "needs_human": False, "action": "detail", "incident_id": "INC-20260513-79ED5E", "event_at": datetime(2026, 5, 18, 7, 31, 37), "channel_type": "telegram", "message_type": "final", "send_status": "sent", "send_error": None, "provider_message_id": "123", "triggered_by_state": "callback_reply", "content_preview": "事件詳情", "run_state": "completed", "agent_id": "legacy-telegram-gateway", "run_created_at": datetime(2026, 5, 18, 7, 30, 0), "callback_reply": { "status": "callback_reply_fallback_sent", "action": "detail", "incident_id": "INC-20260513-79ED5E", }, "awooop_status_chain": { "schema_version": "awooop_status_chain_v1", "repair_state": "read_only_dry_run", "needs_human": True, }, "persisted_awooop_status_chain": { "schema_version": "awooop_status_chain_callback_reply_snapshot_v1", "source_schema_version": "awooop_status_chain_v1", "source": "telegram_callback_reply_snapshot", "source_id": "INC-20260513-79ED5E", "incident_ids": ["INC-20260513-79ED5E"], "current_stage": "approval_required", "stage_status": "waiting", "verdict": "approval_required", "repair_state": "read_only_dry_run", "verification": "missing", "needs_human": True, "next_step": "approve_or_escalate_from_awooop", "evidence": { "auto_repair_records": 0, "operation_records": 0, "mcp_gateway_total": 1, "knowledge_entries": 0, }, "writes": { "incident": False, "auto_repair": False, }, }, "km_stale_completion_summary": { "schema_version": ( "km_stale_owner_review_completion_callback_summary_v1" ), "project_id": "awoooi", "incident_id": "INC-20260513-79ED5E", "status": "matched_owner_review", "ready_count": 3, "blocked_count": 1, "completed_count": 2, "failed_count": 0, "batch_writes_allowed": False, "manual_review_required": True, "related_total": 1, "work_item": None, "related_items": [ { "entry_id": "km-1", "readiness": "ready", "next_action": "preview_stale_km_review_completion", } ], }, "persisted_km_stale_completion_summary": { "schema_version": ( "km_stale_owner_review_callback_reply_snapshot_v1" ), "source_schema_version": ( "km_stale_owner_review_completion_callback_summary_v1" ), "project_id": "awoooi", "incident_id": "INC-20260513-79ED5E", "status": "matched_owner_review", "ready_count": 3, "blocked_count": 1, "completed_count": 2, "failed_count": 0, "batch_writes_allowed": False, "manual_review_required": True, "related_total": 1, "triage": { "flow_stage": "callback_observed_owner_review_link_missing", "ai_lead_agent": "Hermes", "automation_state": "manual_owner_review_required", }, }, "evidence_capture_status": { "schema_version": "callback_evidence_capture_status_v1", "status": "captured", "reason": "ok", "action": "detail", "captured": [ "awooop_status_chain", "km_stale_completion_summary", ], "missing": [], "snapshot_rollout": "t167_t169", "next_action": "none", "event_at": datetime(2026, 5, 18, 7, 31, 37), }, "run_detail_href": ( "/awooop/runs/5c0306e0-591a-5445-9a33-80f499426b38" "?project_id=awoooi" ), } ], "total": 1, "page": 1, "per_page": 20, "summary": { "schema_version": "telegram_callback_reply_audit_summary_v1", "project_id": "awoooi", "outbound_total": 120, "outbound_source_envelope_total": 118, "outbound_source_refs_total": 100, "outbound_trace_ref_total": 92, "outbound_incident_ref_total": 80, "outbound_reply_markup_total": 30, "outbound_reply_markup_missing_incident_ref_total": 4, "outbound_reply_markup_missing_incident_ref_recent_1h_total": 1, "outbound_reply_markup_missing_incident_ref_recent_24h_total": 2, "outbound_reply_markup_missing_incident_ref_latest_sent_at": ( datetime(2026, 5, 18, 8, 15, 0) ), "outbound_reply_markup_missing_trace_ref_total": 2, "outbound_reply_markup_missing_trace_ref_recent_1h_total": 0, "outbound_reply_markup_missing_trace_ref_recent_24h_total": 1, "outbound_reply_markup_missing_trace_ref_latest_sent_at": ( datetime(2026, 5, 18, 7, 40, 0) ), "outbound_reply_markup_trace_ref_gap_status": "recent_backlog", "outbound_reply_markup_trace_ref_gap_next_action": "watch_24h_decay", "outbound_reply_markup_trace_ref_after_gap_total": 3, "outbound_reply_markup_trace_ref_after_gap_first_sent_at": ( datetime(2026, 5, 18, 8, 20, 0) ), "outbound_reply_markup_trace_ref_after_gap_latest_sent_at": ( datetime(2026, 5, 18, 9, 0, 0) ), "outbound_reply_markup_trace_ref_gap_recovery_status": ( "recovered_after_gap" ), "outbound_reply_markup_missing_incident_ref_top_prefixes": [ { "prefix": "silence", "total": 3, "recent_24h_total": 0, "last_sent_at": datetime(2026, 5, 18, 7, 40, 0), }, { "prefix": "drift_view", "total": 1, "recent_24h_total": 1, "last_sent_at": datetime(2026, 5, 18, 8, 15, 0), }, ], "outbound_reply_markup_missing_trace_ref_top_prefixes": [ { "prefix": "silence", "total": 2, "recent_24h_total": 0, "last_sent_at": datetime(2026, 5, 18, 7, 40, 0), }, ], "outbound_failed_total": 1, "callback_total": 3, "callback_sent_total": 1, "callback_fallback_total": 1, "callback_rescue_total": 0, "callback_failed_total": 1, "callback_detail_total": 2, "callback_history_total": 1, "callback_snapshot_captured_total": 1, "callback_snapshot_partial_total": 1, "callback_snapshot_missing_total": 1, "callback_incident_total": 2, "snapshot_status": "partial", "next_action": "review_legacy_callback_snapshot_gap", "latest_outbound_at": datetime(2026, 5, 18, 7, 40, 0), "latest_callback_at": datetime(2026, 5, 18, 7, 31, 37), }, }) dumped = response.model_dump(mode="json") assert dumped["items"][0]["status"] == "fallback_sent" assert dumped["items"][0]["callback_reply"]["action"] == "detail" assert dumped["items"][0]["awooop_status_chain"]["repair_state"] == ( "read_only_dry_run" ) assert dumped["items"][0]["persisted_awooop_status_chain"]["next_step"] == ( "approve_or_escalate_from_awooop" ) assert dumped["items"][0]["km_stale_completion_summary"]["ready_count"] == 3 assert dumped["items"][0]["km_stale_completion_summary"]["related_total"] == 1 assert dumped["items"][0]["persisted_km_stale_completion_summary"]["triage"][ "ai_lead_agent" ] == "Hermes" assert dumped["items"][0]["evidence_capture_status"]["status"] == "captured" assert dumped["items"][0]["run_detail_href"].endswith("project_id=awoooi") assert dumped["summary"]["outbound_total"] == 120 assert dumped["summary"]["outbound_trace_ref_total"] == 92 assert dumped["summary"]["outbound_reply_markup_total"] == 30 assert dumped["summary"]["outbound_reply_markup_missing_incident_ref_total"] == 4 assert dumped["summary"][ "outbound_reply_markup_missing_incident_ref_recent_1h_total" ] == 1 assert dumped["summary"][ "outbound_reply_markup_missing_incident_ref_latest_sent_at" ] == "2026-05-18T08:15:00" assert dumped["summary"]["outbound_reply_markup_missing_trace_ref_total"] == 2 assert dumped["summary"][ "outbound_reply_markup_missing_trace_ref_recent_24h_total" ] == 1 assert dumped["summary"][ "outbound_reply_markup_missing_trace_ref_latest_sent_at" ] == "2026-05-18T07:40:00" assert dumped["summary"]["outbound_reply_markup_trace_ref_gap_status"] == ( "recent_backlog" ) assert dumped["summary"]["outbound_reply_markup_trace_ref_gap_next_action"] == ( "watch_24h_decay" ) assert dumped["summary"]["outbound_reply_markup_trace_ref_after_gap_total"] == 3 assert dumped["summary"][ "outbound_reply_markup_trace_ref_after_gap_first_sent_at" ] == "2026-05-18T08:20:00" assert dumped["summary"][ "outbound_reply_markup_trace_ref_after_gap_latest_sent_at" ] == "2026-05-18T09:00:00" assert dumped["summary"][ "outbound_reply_markup_trace_ref_gap_recovery_status" ] == "recovered_after_gap" assert dumped["summary"][ "outbound_reply_markup_missing_incident_ref_top_prefixes" ][0] == { "prefix": "silence", "total": 3, "recent_24h_total": 0, "first_sent_at": None, "last_sent_at": "2026-05-18T07:40:00", } assert dumped["summary"][ "outbound_reply_markup_missing_trace_ref_top_prefixes" ][0] == { "prefix": "silence", "total": 2, "recent_24h_total": 0, "first_sent_at": None, "last_sent_at": "2026-05-18T07:40:00", } assert dumped["summary"]["callback_snapshot_missing_total"] == 1 assert dumped["summary"]["snapshot_status"] == "partial" def test_list_callback_replies_keeps_audit_summary_separate_from_km_summary() -> None: source = inspect.getsource(platform_operator_service.list_callback_replies) assert "audit_summary = await _fetch_callback_reply_audit_summary" in source assert '"summary": audit_summary' in source assert "km_summary = km_completion_summary_cache.get" in source assert 'item["km_stale_completion_summary"] = km_summary' in source def test_list_callback_replies_does_not_shadow_summary_cache_key() -> None: source = inspect.getsource(platform_operator_service.list_callback_replies) assert "callback_summary_cache_key = {" in source assert "status_chain_cache_key = (item_project_id, incident_id)" in source assert "km_summary_cache_key = (item_project_id, incident_id)" in source assert ( 'store_operator_summary_async(\n "callback_replies",\n' " callback_summary_cache_key" ) in source assert not any( line.strip().startswith("summary_cache_key =") for line in source.splitlines() ) def test_callback_reply_audit_summary_marks_missing_snapshots() -> None: summary = _callback_reply_audit_summary_from_row( { "outbound_total": 5256, "outbound_source_envelope_total": 5256, "outbound_source_refs_total": 5000, "outbound_trace_ref_total": 4300, "outbound_incident_ref_total": 3200, "outbound_reply_markup_total": 100, "outbound_reply_markup_missing_incident_ref_total": 12, "outbound_reply_markup_missing_incident_ref_recent_1h_total": 2, "outbound_reply_markup_missing_incident_ref_recent_24h_total": 3, "outbound_reply_markup_missing_incident_ref_latest_sent_at": ( datetime(2026, 5, 25, 8, 42, 22) ), "outbound_reply_markup_missing_trace_ref_total": 5, "outbound_reply_markup_missing_trace_ref_recent_1h_total": 1, "outbound_reply_markup_missing_trace_ref_recent_24h_total": 2, "outbound_reply_markup_missing_trace_ref_latest_sent_at": ( datetime(2026, 5, 25, 8, 42, 22) ), "outbound_reply_markup_trace_ref_after_gap_total": 0, "outbound_reply_markup_trace_ref_after_gap_first_sent_at": None, "outbound_reply_markup_trace_ref_after_gap_latest_sent_at": None, "outbound_reply_markup_missing_incident_ref_top_prefixes": [ { "prefix": "silence", "total": 8, "recent_24h_total": 0, "last_sent_at": datetime(2026, 5, 18, 7, 40, 0), }, { "prefix": "drift_view", "total": 4, "recent_24h_total": 2, "last_sent_at": datetime(2026, 5, 25, 8, 42, 22), }, ], "outbound_reply_markup_missing_trace_ref_top_prefixes": [ { "prefix": "unknown", "total": 5, "recent_24h_total": 1, "last_sent_at": datetime(2026, 5, 25, 8, 42, 22), }, ], "outbound_failed_total": 0, "callback_total": 2, "callback_sent_total": 2, "callback_fallback_total": 0, "callback_rescue_total": 0, "callback_failed_total": 0, "callback_detail_total": 0, "callback_history_total": 2, "callback_snapshot_captured_total": 0, "callback_snapshot_partial_total": 0, "callback_snapshot_missing_total": 2, "callback_incident_total": 1, "latest_outbound_at": datetime(2026, 5, 25, 8, 42, 22), "latest_callback_at": datetime(2026, 5, 24, 14, 38, 4), }, project_id="awoooi", ) assert summary["schema_version"] == "telegram_callback_reply_audit_summary_v1" assert summary["outbound_total"] == 5256 assert summary["outbound_trace_ref_total"] == 4300 assert summary["outbound_reply_markup_missing_trace_ref_total"] == 5 assert summary["outbound_reply_markup_missing_trace_ref_recent_1h_total"] == 1 assert summary["outbound_reply_markup_missing_trace_ref_recent_24h_total"] == 2 assert summary["outbound_reply_markup_missing_trace_ref_latest_sent_at"] == ( datetime(2026, 5, 25, 8, 42, 22) ) assert summary["outbound_reply_markup_trace_ref_gap_status"] == "active_gap" assert summary["outbound_reply_markup_trace_ref_gap_next_action"] == ( "inspect_recent_outbound_source_refs" ) assert summary["outbound_reply_markup_trace_ref_after_gap_total"] == 0 assert summary["outbound_reply_markup_trace_ref_gap_recovery_status"] == ( "no_recovery_signal" ) assert summary[ "outbound_reply_markup_missing_incident_ref_recent_24h_total" ] == 3 assert summary["outbound_reply_markup_missing_trace_ref_top_prefixes"][0][ "prefix" ] == "unknown" assert summary["callback_total"] == 2 assert summary["callback_snapshot_missing_total"] == 2 assert summary["inbound_callback_total"] == 0 assert summary["inbound_callback_mirror_status"] == "reply_only_gap" assert summary["inbound_callback_next_action"] == ( "press_any_telegram_callback_after_rollout" ) assert summary["snapshot_status"] == "not_captured" assert summary["next_action"] == "press_telegram_detail_or_history_after_rollout" assert summary["outbound_reply_markup_missing_incident_ref_top_prefixes"][0][ "recent_24h_total" ] == 0 assert summary["outbound_reply_markup_missing_incident_ref_top_prefixes"][1][ "last_sent_at" ] == datetime(2026, 5, 25, 8, 42, 22) def test_callback_reply_audit_summary_marks_mixed_legacy_snapshots_partial() -> None: summary = _callback_reply_audit_summary_from_row( { "outbound_total": 5221, "outbound_source_envelope_total": 4905, "outbound_source_refs_total": 4676, "outbound_trace_ref_total": 4230, "outbound_incident_ref_total": 920, "outbound_reply_markup_total": 1322, "outbound_reply_markup_missing_incident_ref_total": 684, "outbound_reply_markup_missing_incident_ref_recent_1h_total": 0, "outbound_reply_markup_missing_incident_ref_recent_24h_total": 0, "outbound_reply_markup_missing_incident_ref_latest_sent_at": ( datetime(2026, 5, 25, 10, 59, 49) ), "outbound_reply_markup_missing_trace_ref_total": 154, "outbound_reply_markup_missing_trace_ref_recent_1h_total": 0, "outbound_reply_markup_missing_trace_ref_recent_24h_total": 23, "outbound_reply_markup_missing_trace_ref_latest_sent_at": ( datetime(2026, 5, 25, 12, 13, 1) ), "outbound_reply_markup_trace_ref_after_gap_total": 8, "outbound_reply_markup_trace_ref_after_gap_first_sent_at": ( datetime(2026, 5, 25, 12, 20, 0) ), "outbound_reply_markup_trace_ref_after_gap_latest_sent_at": ( datetime(2026, 5, 25, 13, 26, 8) ), "outbound_reply_markup_missing_incident_ref_top_prefixes": [ { "prefix": "silence", "total": 275, "recent_24h_total": 0, "last_sent_at": datetime(2026, 5, 25, 10, 59, 49), }, { "prefix": "drift_view", "total": 144, "recent_24h_total": 0, "last_sent_at": datetime(2026, 5, 18, 18, 14, 27), }, ], "outbound_reply_markup_missing_trace_ref_top_prefixes": [ { "prefix": "silence", "total": 120, "recent_24h_total": 23, "last_sent_at": datetime(2026, 5, 25, 12, 13, 1), }, { "prefix": "ai_advisory_handled", "total": 34, "recent_24h_total": 23, "last_sent_at": datetime(2026, 5, 25, 12, 7, 17), }, ], "outbound_failed_total": 0, "callback_total": 3, "callback_sent_total": 3, "callback_fallback_total": 0, "callback_rescue_total": 0, "callback_failed_total": 0, "callback_detail_total": 0, "callback_history_total": 3, "callback_snapshot_captured_total": 1, "callback_snapshot_partial_total": 0, "callback_snapshot_missing_total": 2, "callback_incident_total": 1, "latest_outbound_at": datetime(2026, 5, 25, 9, 15, 14), "latest_callback_at": datetime(2026, 5, 25, 9, 30, 0), }, project_id="awoooi", ) assert summary["callback_snapshot_captured_total"] == 1 assert summary["outbound_trace_ref_total"] == 4230 assert summary["outbound_reply_markup_missing_trace_ref_total"] == 154 assert summary["outbound_reply_markup_missing_trace_ref_recent_1h_total"] == 0 assert summary["outbound_reply_markup_missing_trace_ref_recent_24h_total"] == 23 assert summary["outbound_reply_markup_missing_trace_ref_latest_sent_at"] == ( datetime(2026, 5, 25, 12, 13, 1) ) assert summary["outbound_reply_markup_trace_ref_gap_status"] == "recent_backlog" assert summary["outbound_reply_markup_trace_ref_gap_next_action"] == ( "watch_24h_decay" ) assert summary["outbound_reply_markup_trace_ref_after_gap_total"] == 8 assert summary["outbound_reply_markup_trace_ref_after_gap_first_sent_at"] == ( datetime(2026, 5, 25, 12, 20, 0) ) assert summary["outbound_reply_markup_trace_ref_gap_recovery_status"] == ( "recovered_after_gap" ) assert summary["outbound_reply_markup_missing_trace_ref_top_prefixes"][0][ "recent_24h_total" ] == 23 assert summary["callback_snapshot_missing_total"] == 2 assert summary["snapshot_status"] == "partial" assert summary["next_action"] == "review_legacy_callback_snapshot_gap" assert summary["outbound_reply_markup_missing_incident_ref_top_prefixes"][0][ "last_sent_at" ] == datetime(2026, 5, 25, 10, 59, 49) @pytest.mark.asyncio async def test_km_stale_completion_summary_matches_callback_incident( monkeypatch, ) -> None: async def fake_query_km_stale_completion_queue(**kwargs): assert kwargs["project_id"] == "awoooi" assert kwargs["status_bucket"] == "all" assert kwargs["limit"] == 100 return SimpleNamespace( project_id="awoooi", total=2, returned=2, pending_count=2, ready_count=1, blocked_count=1, completed_count=0, failed_count=0, writes_on_read=False, manual_review_required=True, batch_writes_allowed=False, items=[ SimpleNamespace( entry_id="km-1", title="Bitan pharmacy status drift", dispatch_id="dispatch-1", governance_event_id="event-1", readiness="ready", workflow_stage="waiting_owner_review", next_action="preview_stale_km_review_completion", priority_tier="P0", recommended_completion_outcome="refresh_with_evidence", can_preview=True, related_incident_id="INC-20260513-79ED5E", ), SimpleNamespace( entry_id="km-2", title="Other stale KM", related_incident_id="INC-20260513-OTHER", readiness="blocked", ), ], ) monkeypatch.setattr( platform_operator_service, "query_km_stale_owner_review_completion_queue", fake_query_km_stale_completion_queue, ) summary = await platform_operator_service._fetch_km_stale_completion_summary_for_incident( project_id="awoooi", incident_id="INC-20260513-79ED5E", queue_cache={}, ) assert summary["schema_version"] == ( "km_stale_owner_review_completion_callback_summary_v1" ) assert summary["status"] == "matched_owner_review" assert summary["ready_count"] == 1 assert summary["blocked_count"] == 1 assert summary["batch_writes_allowed"] is False assert summary["related_items"][0]["entry_id"] == "km-1" assert summary["related_items"][0]["can_preview"] is True assert summary["work_item"] is None def test_km_stale_completion_summary_generates_owner_review_work_item() -> None: summary = platform_operator_service._build_km_stale_completion_summary( queue=SimpleNamespace( total=10, returned=10, pending_count=10, ready_count=10, blocked_count=0, completed_count=1, failed_count=0, writes_on_read=False, manual_review_required=True, batch_writes_allowed=False, items=[], ), project_id="awoooi", incident_id="INC-20260524-16109D", ) assert summary["status"] == "no_related_owner_review" assert summary["work_item"] == { "schema_version": "km_stale_callback_owner_review_work_item_v1", "work_item_id": ( "km-callback-owner-review:awoooi:INC-20260524-16109D" ), "kind": "km_stale_callback_owner_review", "status": "open", "project_id": "awoooi", "incident_id": "INC-20260524-16109D", "reason": "no_matching_completion_item", "title": ( "Telegram callback incident has no matching KM owner-review item" ), "next_step": "review_or_queue_km_owner_review", "target_surface": "awooop_runs_callback_evidence", "target_href": ( "/awooop/runs?project_id=awoooi" "&incident_id=INC-20260524-16109D" "&callback_reply_status=sent" ), "work_item_href": ( "/awooop/work-items?project_id=awoooi" "&work_item_id=km-callback-owner-review%3Aawoooi%3AINC-20260524-16109D" "&incident_id=INC-20260524-16109D" ), "triage": { "schema_version": "km_stale_callback_owner_review_triage_v1", "flow_stage": "callback_observed_owner_review_link_missing", "ai_lead_agent": "Hermes", "supporting_agents": ["OpenClaw", "ElephantAlpha"], "automation_state": "manual_owner_review_required", "safe_to_auto_repair": False, "blocking_reason": "no_matching_completion_item", "matching_strategy": "related_incident_id_exact_match", "already_done": [ "callback_reply_persisted", "completion_queue_checked", "generated_read_only_work_item", ], "next_actions": [ "review_runs_callback_evidence", "queue_matching_km_stale_candidate", "complete_owner_review_after_owner_approval", ], }, "writes_on_read": False, "manual_review_required": True, "batch_writes_allowed": False, } def test_list_approvals_response_preserves_status_chain() -> None: run_id = UUID("5c0306e0-591a-5445-9a33-80f499426b38") response = ListApprovalsResponse.model_validate({ "items": [ { "run_id": run_id, "project_id": "awoooi", "agent_id": "hermes-approval-router", "created_at": datetime(2026, 5, 18, 7, 30, 0), "timeout_at": datetime(2026, 5, 18, 7, 45, 0), "remediation_summary": { "status": "read_only_dry_run", "incident_ids": ["INC-20260513-79ED5E"], }, "awooop_status_chain": { "schema_version": "awooop_status_chain_v1", "source_id": "INC-20260513-79ED5E", "repair_state": "read_only_dry_run", "needs_human": True, "next_step": "approve_or_escalate_from_awooop", }, } ], "total": 1, }) dumped = response.model_dump(mode="json") assert dumped["items"][0]["remediation_summary"]["status"] == ( "read_only_dry_run" ) assert dumped["items"][0]["awooop_status_chain"]["source_id"] == ( "INC-20260513-79ED5E" ) assert dumped["items"][0]["awooop_status_chain"]["needs_human"] is True def test_callback_reply_action_filter_normalizes_safe_actions() -> None: assert _validate_callback_reply_action_filter(" History ") == "history" assert _validate_callback_reply_action_filter("incident:detail-2") == ( "incident:detail-2" ) assert _validate_callback_reply_action_filter("") is None def test_callback_reply_action_filter_rejects_unsafe_values() -> None: with pytest.raises(HTTPException): _validate_callback_reply_action_filter("detail;drop") def test_remediation_timeline_summary_surfaces_route_and_write_flags() -> None: summary = _remediation_timeline_summary({ "incident_id": "INC-20260514-F85F21", "mode": "replay", "verification_result_preview": "degraded", "agent_id": "auto_repair_executor", "tool_name": "ssh_diagnose", "required_scope": "read", "writes_incident_state": False, "writes_auto_repair_result": False, }) assert "incident=INC-20260514-F85F21" in summary assert "route=auto_repair_executor/ssh_diagnose/read" in summary assert "writes_incident=False" in summary assert "writes_auto_repair=False" in summary def test_awooop_status_chain_marks_verified_repair() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-20260513-79ED5E"], source_id="INC-20260513-79ED5E", truth_chain={ "truth_status": { "current_stage": "execution_succeeded", "stage_status": "success", "needs_human": False, "blockers": [], }, "automation_quality": { "verdict": "auto_repaired_verified", "facts": { "auto_repair_execution_records": 1, "automation_operation_records": 1, "verification_result": "healthy", "mcp_gateway_total": 2, "knowledge_entries": 1, }, "blockers": [], }, "mcp": { "awooop_gateway": { "total": 2, "success": 1, "failed": 1, "blocked": 0, "first_class_total": 2, "legacy_bridge_total": 0, "policy_enforced_total": 2, "stage": "provider_failed_after_gateway", "stage_status": "failed", "by_tool": [ { "tool_name": "prometheus.query", "total": 2, "success": 1, "failed": 1, "blocked": 0, } ], }, "legacy": { "total": 1, "success": 1, "failed": 0, "by_tool": [ { "tool_name": "ssh_host", "success": 1, "failed": 0, } ], }, }, "execution": { "automation_operation_log": [ { "operation_type": "playbook_executed", "status": "success", "actor": "auto_repair_executor", "input_action": "restart_service", "input_executor": "ansible", "input_playbook_id": "pb-host-restart", "input_playbook_path": "infra/ansible/playbooks/188-ai-web.yml", } ], "ansible": { "considered": True, "records": [ { "operation_type": "ansible_check_mode_executed", "status": "success", "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", "check_mode": "true", } ], "candidate_catalog": { "candidates": [ { "catalog_id": "ansible:188-ai-web", "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", "risk_level": "medium", "match_score": 3, } ] }, "not_used_reason": None, }, }, "channel": { "inbound_events": [ { "channel_type": "alertmanager", "provider_event_id": "alert-1", "content_type": "application/json", "is_duplicate": False, "received_at": "2026-05-20T00:00:00Z", "source_envelope": { "source_refs": { "alert_ids": ["alert-1"], "sentry_issue_ids": ["SENTRY-1"], "signoz_alerts": ["signoz:abc"], "fingerprints": ["fp-1"], } }, } ], "outbound_messages": [ { "channel_type": "telegram", "message_type": "incident_detail", "send_status": "sent", "sent_at": "2026-05-20T00:01:00Z", } ], }, }, remediation_history={ "total": 1, "items": [ { "incident_id": "INC-20260513-79ED5E", "agent_id": "auto_repair_executor", "tool_name": "rollout_restart", "required_scope": "write", "verification_result_preview": "healthy", "writes_incident_state": True, "writes_auto_repair_result": True, } ], }, ) assert chain["repair_state"] == "auto_repaired_verified" assert chain["verification"] == "healthy" assert chain["needs_human"] is False assert chain["next_step"] == "monitor_for_regression" assert chain["operator_outcome"]["state"] == "completed_verified" assert chain["operator_outcome"]["needs_human"] is False assert chain["evidence"]["latest_route"] == "auto_repair_executor/rollout_restart/write" assert chain["mcp"]["gateway"]["success"] == 1 assert chain["mcp"]["gateway"]["failed"] == 1 assert chain["mcp"]["gateway"]["policy_enforced_total"] == 2 assert chain["mcp"]["legacy"]["total"] == 1 assert chain["mcp"]["top_tools"][0]["tool_name"] == "prometheus.query" assert chain["execution"]["operation_total"] == 1 assert chain["execution"]["latest_executor"] == "ansible" assert chain["execution"]["playbook_ids"] == ["pb-host-restart"] assert chain["execution"]["ansible"]["considered"] is True assert chain["execution"]["ansible"]["candidate_count"] == 1 assert chain["execution"]["ansible"]["check_mode_total"] == 1 assert chain["execution"]["ansible"]["apply_total"] == 0 assert chain["source_refs"]["inbound_total"] == 1 assert chain["source_refs"]["outbound_total"] == 1 assert chain["source_refs"]["refs"]["sentry_issue_ids"] == ["SENTRY-1"] assert chain["source_refs"]["refs"]["signoz_alerts"] == ["signoz:abc"] def test_awooop_status_chain_surfaces_controlled_ansible_apply_proof() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-20260531-D6A3C4"], source_id="INC-20260531-D6A3C4", truth_chain={ "truth_status": { "current_stage": "execution_succeeded", "stage_status": "success", "needs_human": False, "blockers": [], }, "automation_quality": { "verdict": "execution_succeeded", "facts": { "auto_repair_execution_records": 0, "automation_operation_records": 2, "effective_execution_records": 1, "verification_result": "healthy", "mcp_gateway_total": 2, "knowledge_entries": 1, }, "blockers": [], }, "execution": { "automation_operation_log": [ { "operation_type": "ansible_apply_executed", "status": "success", "actor": "platform_operator", "input_executor": "ansible", "input_catalog_id": "ansible:188-momo-backup-user", "input_playbook_path": "infra/ansible/playbooks/188-momo-backup-user.yml", } ], "ansible": { "considered": True, "records": [ { "operation_type": "ansible_apply_executed", "status": "success", "actor": "platform_operator", "catalog_id": "ansible:188-momo-backup-user", "playbook_path": "infra/ansible/playbooks/188-momo-backup-user.yml", "execution_mode": "apply", "check_mode": False, "apply_executed": True, "approval_source": "user_chat_approved_continue", "returncode": 0, }, { "operation_type": "ansible_check_mode_executed", "status": "success", "actor": "ansible_check_mode_worker", "catalog_id": "ansible:188-momo-backup-user", "playbook_path": "infra/ansible/playbooks/188-momo-backup-user.yml", "execution_mode": "check_mode", "check_mode": True, "apply_executed": False, "returncode": 0, }, ], "candidate_catalog": {"candidates": []}, }, }, }, remediation_history={"total": 0}, ) ansible = chain["execution"]["ansible"] assert ansible["check_mode_total"] == 1 assert ansible["apply_total"] == 1 assert ansible["applied"] is True assert ansible["controlled_apply"] is True assert ansible["latest_catalog_id"] == "ansible:188-momo-backup-user" assert ansible["latest_execution_mode"] == "apply" assert ansible["latest_returncode"] == 0 assert ansible["approval_source"] == "user_chat_approved_continue" def test_awooop_status_chain_includes_source_provider_correlation() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-20260520-4D1124"], source_id="INC-20260520-4D1124", source_correlation={ "schema_version": "source_provider_correlation_v1", "status": "candidate_found", "verification_status": "candidate_only", "direct_ref_total": 0, "candidate_total": 2, "applied_link_total": 0, "provider_event_total": 2, "providers": { "sentry": { "direct_ref_total": 0, "candidate_total": 1, "applied_link_total": 0, }, "signoz": { "direct_ref_total": 0, "candidate_total": 1, "applied_link_total": 0, }, }, "top_candidates": [ { "provider": "sentry", "provider_event_id": "sentry:issue:1", "score": 65, "match_type": "candidate", "reasons": ["alertname_overlap", "target_overlap"], } ], }, ) correlation = chain["source_refs"]["correlation"] assert correlation["status"] == "candidate_found" assert correlation["candidate_total"] == 2 assert correlation["verification_status"] == "candidate_only" assert correlation["providers"]["sentry"]["candidate_total"] == 1 assert correlation["top_candidates"][0]["provider_event_id"] == "sentry:issue:1" def test_awooop_status_chain_preserves_applied_source_link_verification() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-20260520-4D1124"], source_id="INC-20260520-4D1124", source_correlation={ "schema_version": "source_provider_correlation_v1", "status": "linked", "verification_status": "applied_link_verified", "direct_ref_total": 1, "candidate_total": 0, "applied_link_total": 1, "latest_applied_link_at": "2026-05-21T02:03:04", "provider_event_total": 1, "providers": { "sentry": { "direct_ref_total": 1, "candidate_total": 0, "applied_link_total": 1, "latest_applied_link_at": "2026-05-21T02:03:04", }, }, "top_candidates": [ { "provider": "sentry", "provider_event_id": "sentry:source_correlation_linked:issue-1", "stage": "source_correlation_linked", "score": 100, "match_type": "direct", "link_state": "applied", "verification_status": "applied_link_verified", "reasons": ["direct_incident_ref"], } ], }, ) correlation = chain["source_refs"]["correlation"] assert correlation["status"] == "linked" assert correlation["verification_status"] == "applied_link_verified" assert correlation["applied_link_total"] == 1 assert correlation["latest_applied_link_at"] == "2026-05-21T02:03:04" assert correlation["providers"]["sentry"]["applied_link_total"] == 1 assert correlation["top_candidates"][0]["link_state"] == "applied" def test_source_correlation_scoring_distinguishes_direct_and_candidate() -> None: incident_context = { "incident_ids": ["INC-20260520-4D1124"], "alertnames": ["highcpuusage"], "severities": ["p3"], "fingerprints": ["fp-abc"], "namespaces": ["awoooi-prod"], "targets": ["api"], } direct_event = { "incident_ids": ["INC-20260520-4D1124"], "alertnames": ["other"], "severities": [], "fingerprints": [], "namespaces": [], "targets": [], } candidate_event = { "incident_ids": [], "alertnames": ["highcpuusage"], "severities": ["p3"], "fingerprints": [], "namespaces": ["awoooi-prod"], "targets": ["api"], } unrelated_event = { "incident_ids": [], "alertnames": ["configdrift"], "severities": ["p3"], "fingerprints": [], "namespaces": ["awoooi-prod"], "targets": [], } direct = _score_source_correlation_event(incident_context, direct_event) candidate = _score_source_correlation_event(incident_context, candidate_event) unrelated = _score_source_correlation_event(incident_context, unrelated_event) assert direct["is_direct"] is True assert direct["is_candidate"] is True assert candidate["is_direct"] is False assert candidate["is_candidate"] is True assert "alertname_overlap" in candidate["reasons"] assert unrelated["is_candidate"] is False def test_source_correlation_applied_link_requires_stage_and_direct_match() -> None: incident_context = { "incident_ids": ["INC-20260520-4D1124"], "alertnames": ["sentry issue"], "severities": ["error"], "fingerprints": ["fp-sentry-1"], "namespaces": ["awoooi-prod"], "targets": ["web"], } event_context = _source_event_correlation_context({ "provider": "sentry", "provider_event_id": "sentry:source_correlation_linked:issue-1", "received_at": datetime(2026, 5, 21, 2, 3, 4), "source_envelope": { "provider": "sentry", "stage": "source_correlation_linked", "source_refs": { "incident_ids": ["INC-20260520-4D1124"], "fingerprints": ["fp-sentry-1"], }, "log_correlation": { "alertname": "Sentry Issue", "severity": "error", "namespace": "awoooi-prod", "target_resource": "web", }, }, }) scored = _score_source_correlation_event(incident_context, event_context) assert scored["is_direct"] is True assert _is_source_correlation_applied_link(event_context, scored) is True candidate_only_context = { **event_context, "incident_ids": [], "fingerprints": [], } candidate_only = _score_source_correlation_event( incident_context, candidate_only_context, ) non_link_stage = { **event_context, "stage": "upstream_canary", } assert candidate_only["is_direct"] is False assert candidate_only["is_candidate"] is True assert _is_source_correlation_applied_link( candidate_only_context, candidate_only, ) is False assert _is_source_correlation_applied_link(non_link_stage, scored) is False def test_awooop_status_chain_marks_read_only_manual_gate() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-20260513-79ED5E"], source_id="INC-20260513-79ED5E", truth_chain={ "truth_status": { "current_stage": "approval_required", "stage_status": "waiting", "needs_human": True, "blockers": ["pending_human_approval"], }, "automation_quality": { "verdict": "approval_required", "facts": { "auto_repair_execution_records": 0, "automation_operation_records": 0, "verification_result": "missing", "mcp_gateway_total": 1, "knowledge_entries": 0, }, "blockers": [], }, }, remediation_history={ "total": 2, "items": [ { "incident_id": "INC-20260513-79ED5E", "agent_id": "investigator", "tool_name": "ssh_diagnose", "required_scope": "read", "verification_result_preview": "degraded", "writes_incident_state": False, "writes_auto_repair_result": False, } ], }, ) assert chain["repair_state"] == "read_only_dry_run" assert chain["needs_human"] is True assert chain["next_step"] == "approve_or_escalate_from_awooop" assert chain["blockers"] == ["pending_human_approval"] def test_awooop_status_chain_does_not_treat_audit_ops_as_repair() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-20260530-88D960"], source_id="INC-20260530-88D960", truth_chain={ "truth_status": { "current_stage": "execution_succeeded", "stage_status": "success", "needs_human": False, "blockers": [], }, "automation_quality": { "verdict": "auto_repaired_verification_degraded", "facts": { "auto_repair_execution_records": 0, "automation_operation_records": 1, "effective_execution_records": 0, "verification_result": "degraded", "mcp_gateway_total": 22, "knowledge_entries": 4, }, "blockers": ["verification_recorded"], }, }, remediation_history={"total": 0}, ) assert chain["repair_state"] == "diagnostic_or_audit_recorded" assert chain["next_step"] == "manual_review_or_collect_repair_evidence" assert chain["needs_human"] is True assert chain["operator_outcome"]["state"] == "diagnostic_only_manual_review" assert chain["operator_outcome"]["notification"]["mode"] == "action_required" assert chain["evidence"]["operation_records"] == 1 assert chain["evidence"]["auto_repair_records"] == 0 def test_awooop_status_chain_surfaces_rejected_approval_outcome() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-REJECTED"], source_id="INC-REJECTED", truth_chain={ "truth_status": { "current_stage": "approval_rejected", "stage_status": "closed", "needs_human": False, "blockers": [], }, "automation_quality": { "verdict": "approval_rejected_no_execution", "facts": { "auto_repair_execution_records": 0, "automation_operation_records": 0, "effective_execution_records": 0, "verification_result": "missing", "mcp_gateway_total": 1, "knowledge_entries": 0, }, "blockers": [], }, }, remediation_history={"total": 0}, ) assert chain["repair_state"] == "approval_rejected_no_execution" assert chain["needs_human"] is False assert chain["operator_outcome"]["state"] == "approval_rejected_no_execution" assert chain["operator_outcome"]["notification"]["mode"] == "result_only" def test_awooop_status_chain_surfaces_expired_approval_outcome() -> None: chain = _build_awooop_status_chain( incident_ids=["INC-EXPIRED"], source_id="INC-EXPIRED", truth_chain={ "truth_status": { "current_stage": "approval_expired", "stage_status": "expired", "needs_human": True, "blockers": ["approval_expired_without_operator_decision"], }, "automation_quality": { "verdict": "approval_expired_manual_review", "facts": { "auto_repair_execution_records": 0, "automation_operation_records": 0, "effective_execution_records": 0, "verification_result": "missing", "mcp_gateway_total": 1, "knowledge_entries": 0, }, "blockers": [], }, }, remediation_history={"total": 0}, ) assert chain["repair_state"] == "approval_expired_manual_review" assert chain["needs_human"] is True assert chain["operator_outcome"]["state"] == "approval_expired_manual_review" assert chain["operator_outcome"]["notification"]["mode"] == "action_required" def test_legacy_mcp_timeline_summary_surfaces_tool_context() -> None: record = { "incident_id": "INC-20260514-F85F21", "agent_role": "pre_decision_investigator", "flywheel_node": "investigator", "duration_ms": 127, "success": True, "error_message": None, } assert _legacy_mcp_timeline_status(record) == "success" summary = _legacy_mcp_timeline_summary(record) assert "incident=INC-20260514-F85F21" in summary assert "agent=pre_decision_investigator" in summary assert "node=investigator" in summary assert "duration_ms=127" in summary def test_legacy_mcp_timeline_status_marks_failed_and_unknown() -> None: assert _legacy_mcp_timeline_status({"success": False}) == "failed" assert _legacy_mcp_timeline_status({"success": None}) == "warning" def test_run_remediation_list_summary_marks_read_only_dry_run() -> None: run = SimpleNamespace(state="waiting_approval") summary = _run_remediation_list_summary( run=run, incident_ids=["INC-20260514-F85F21"], items=[ { "created_at": "2026-05-14T23:04:00+00:00", "incident_id": "INC-20260514-F85F21", "mode": "replay", "verification_result_preview": "degraded", "agent_id": "auto_repair_executor", "tool_name": "ssh_diagnose", "required_scope": "read", "writes_incident_state": False, "writes_auto_repair_result": False, } ], ) assert summary["status"] == "read_only_dry_run" assert summary["has_dry_run"] is True assert summary["is_read_only"] is True assert summary["human_gate_open"] is True assert summary["latest_route"] == "auto_repair_executor/ssh_diagnose/read" def test_run_remediation_list_summary_marks_mcp_observed_without_dry_run() -> None: run = SimpleNamespace(state="completed") summary = _run_remediation_list_summary( run=run, incident_ids=["INC-20260518-792684"], items=[], legacy_mcp_records=[ { "created_at": "2026-05-18T04:31:30+00:00", "incident_id": "INC-20260518-792684", "agent_role": "pre_decision_investigator", "mcp_server": "ssh_host", "tool_name": "ssh_diagnose", "success": True, }, { "created_at": "2026-05-18T04:31:29+00:00", "incident_id": "INC-20260518-792684", "agent_role": "pre_decision_investigator", "mcp_server": "signoz", "tool_name": "query_logs", "success": False, }, ], ) assert summary["status"] == "mcp_observed" assert summary["source"] == "mcp_audit_log" assert summary["total"] == 0 assert summary["evidence_total"] == 2 assert summary["has_dry_run"] is False assert summary["has_mcp_investigation"] is True assert summary["mcp_observation_total"] == 2 assert summary["mcp_observation_success"] == 1 assert summary["mcp_observation_failed"] == 1 assert summary["latest_route"] == "pre_decision_investigator/ssh_host.ssh_diagnose/read" def test_run_remediation_list_summary_flags_write_observed() -> None: run = SimpleNamespace(state="completed") summary = _run_remediation_list_summary( run=run, incident_ids=["INC-20260514-F85F21"], items=[ { "created_at": "2026-05-14T23:05:00+00:00", "incident_id": "INC-20260514-F85F21", "agent_id": "auto_repair_executor", "tool_name": "state_update", "required_scope": "write", "writes_incident_state": True, "writes_auto_repair_result": False, } ], ) assert summary["status"] == "write_observed" assert summary["is_read_only"] is False assert summary["writes_incident_state"] is True def test_remediation_summary_matches_status_filter() -> None: assert _remediation_summary_matches_status( {"status": "mcp_observed"}, "mcp_observed", ) assert _remediation_summary_matches_status( {"status": "read_only_dry_run"}, "read_only_dry_run", ) assert not _remediation_summary_matches_status( {"status": "write_observed"}, "read_only_dry_run", ) assert _remediation_summary_matches_status(None, "no_evidence") def test_callback_reply_summary_matches_status_filter() -> None: assert _callback_reply_summary_matches_status( {"status": "failed"}, "failed", ) assert _callback_reply_summary_matches_status( {"status": "fallback_sent"}, "fallback_sent", ) assert not _callback_reply_summary_matches_status( {"status": "sent"}, "failed", ) assert _callback_reply_summary_matches_status( {"status": "sent"}, "observed", ) assert _callback_reply_summary_matches_status( {"status": "fallback_sent"}, "observed", ) assert _callback_reply_summary_matches_status( {"status": "failed"}, "observed", ) assert not _callback_reply_summary_matches_status(None, "observed") assert _callback_reply_summary_matches_status(None, "no_callback") def test_callback_reply_status_filter_rejects_unknown_value() -> None: _validate_callback_reply_status_filter("failed") with pytest.raises(HTTPException) as exc_info: _validate_callback_reply_status_filter("telegram_error") assert exc_info.value.status_code == 422 assert "callback_reply_status" in str(exc_info.value.detail) def test_list_callback_replies_observed_filter_keeps_delivered_statuses() -> None: source = inspect.getsource(platform_operator_service.list_callback_replies) assert "callback_reply_status == \"observed\"" not in source assert "callback_reply_sent" not in source assert "callback_reply_fallback_sent" not in source assert "callback_reply_rescue_sent" not in source assert "callback_reply_failed" not in source def test_remediation_summary_matches_incident_id_filter() -> None: assert _remediation_summary_matches_incident_id( {"incident_ids": ["INC-20260514-F85F21"]}, "INC-20260514-F85F21", ) assert not _remediation_summary_matches_incident_id( {"incident_ids": ["INC-20260514-F85F21"]}, "INC-20260513-79ED5E", ) assert _remediation_summary_matches_incident_id(None, None) def test_list_filter_context_limit_scales_with_candidate_rows() -> None: assert _list_filter_context_limit(2) == 500 assert _list_filter_context_limit(4176) == 16704 assert _list_filter_context_limit(10000) == 20000 def test_run_context_batches_stay_under_asyncpg_parameter_ceiling() -> None: runs = [ SimpleNamespace( run_id=UUID(int=index + 1), trigger_ref=str(UUID(int=index + 10_000)), ) for index in range((_RUN_CONTEXT_QUERY_CHUNK_SIZE * 2) + 7) ] batches = _iter_run_context_batches(runs) assert [len(batch["run_ids"]) for batch in batches] == [ _RUN_CONTEXT_QUERY_CHUNK_SIZE, _RUN_CONTEXT_QUERY_CHUNK_SIZE, 7, ] assert batches[0]["trigger_ref_to_run"][str(UUID(int=10_000))] == UUID(int=1) for batch in batches: worst_case_inbound_params = ( len(batch["run_ids"]) + len(batch["trigger_refs"]) + len(batch["trigger_event_ids"]) ) assert len(batch["run_ids"]) <= _RUN_CONTEXT_QUERY_CHUNK_SIZE assert worst_case_inbound_params <= _RUN_CONTEXT_QUERY_CHUNK_SIZE * 3 assert worst_case_inbound_params < 32_767 def test_timeline_sort_key_normalizes_datetime_and_iso_string() -> None: fallback = datetime(2026, 5, 14, 10, 0, 0) keys = [ _timeline_sort_key({"ts": datetime(2026, 5, 14, 10, 0, 1)}, fallback), _timeline_sort_key({"ts": "2026-05-14T10:00:02+00:00"}, fallback), _timeline_sort_key({"ts": None}, fallback), ] assert keys == [ "2026-05-14T10:00:01", "2026-05-14T10:00:02+00:00", "2026-05-14T10:00:00", ] assert sorted(keys) == [ "2026-05-14T10:00:00", "2026-05-14T10:00:01", "2026-05-14T10:00:02+00:00", ] def test_ai_route_policy_order_exposes_global_ollama_then_gemini() -> None: policy = _ai_route_policy_order("deep_rca") assert [item["provider_name"] for item in policy] == [ "ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini", ] assert policy[-1]["role"] == "final_fallback" assert policy[-1]["runtime"] == "cloud" def test_ai_route_health_map_marks_standby_as_not_checked() -> None: route = OllamaRoutingResult( primary=OllamaEndpoint( url="http://gcp-a:11434", provider_name="ollama_gcp_a", model="qwen3:14b", ), fallback_chain=[ OllamaEndpoint( url="http://gcp-b:11434", provider_name="ollama_gcp_b", model="qwen3:14b", ), OllamaEndpoint( url="http://local-111:11434", provider_name="ollama_local", model="qwen3:14b", ), ], routing_reason="primary healthy", health_gcp_a=HealthReport( status=HealthStatus.HEALTHY, host="http://gcp-a:11434", latency_ms=123.4, reason="ok", ), health_gcp_b=None, health_local=None, ) health = _ai_route_health_map(route) assert health["ollama_gcp_a"]["status"] == "healthy" assert health["ollama_gcp_a"]["checked"] is True assert health["ollama_gcp_b"]["status"] == "not_checked" assert health["ollama_local"]["reason"] == "standby_not_checked_primary_healthy" def test_ai_route_status_response_preserves_route_fields() -> None: response = AiRouteStatusResponse.model_validate({ "schema_version": "awooop_ai_route_status_v1", "workload_type": "deep_rca", "policy_order": _ai_route_policy_order("deep_rca"), "selected_provider": "ollama_gcp_a", "selected_url": "http://gcp-a:11434", "selected_model": "qwen3:14b", "fallback_chain": [ { "priority": 2, "provider_name": "ollama_gcp_b", "url": "http://gcp-b:11434", "model": "qwen3:14b", "runtime": "ollama", } ], "route_reason": "primary healthy", "route_source": "ollama_failover_manager", "route_error": None, "health": { "ollama_gcp_a": { "status": "healthy", "host": "http://gcp-a:11434", "latency_ms": 123.4, "reason": "ok", "checked_at": 0, "from_cache": False, "checked": True, }, }, "lane_mode": "primary", "active_lane": { "provider_name": "ollama_gcp_a", "health_status": "healthy", "action_required": False, }, "skipped_lanes": [], "operator_action": { "human_required": False, "action": "monitor", "reason": "primary_lane_active", }, "repair_evidence": { "provider": "ai_route_repair", "stage": "repair_diagnosis", "target_resource": "ollama_gcp_a", "access_blockers": ["gcloud_compute_instances_get_missing"], }, "checked_at": datetime(2026, 5, 19, 12, 0, 0), }) dumped = response.model_dump(mode="json") assert dumped["policy_order"][-1]["provider_name"] == "gemini" assert dumped["selected_provider"] == "ollama_gcp_a" assert dumped["lane_mode"] == "primary" assert dumped["repair_evidence"]["target_resource"] == "ollama_gcp_a" def test_ai_route_repair_evidence_item_summarizes_operator_safe_fields() -> None: row = { "event_id": UUID("dff309f0-f159-4537-8f58-47714ce94dca"), "run_id": UUID("ca67ebcc-a24f-53e7-9505-2db15d855ecc"), "provider_event_id": ( "ai_route_repair:repair_diagnosis:" "gcp-a-primary-lane-down-20260525T060415Z" ), "provider_ts": datetime(2026, 5, 25, 6, 4, 15), "received_at": datetime(2026, 5, 25, 6, 5, 3), "source_envelope": { "provider": "ai_route_repair", "stage": "repair_diagnosis", "log_correlation": { "alertname": "GcpAPrimaryLaneDown", "severity": "warning", "target_resource": "ollama_gcp_a", "fingerprint": "ai-route-gcp-a-primary-down", }, "source_refs": { "alert_ids": ["gcp-a-primary-lane-down"], "signoz_alerts": ["signoz:gcp-a"], "sentry_issue_ids": [], "fingerprints": ["ai-route-gcp-a-primary-down"], "run_ids": ["ca67ebcc-a24f-53e7-9505-2db15d855ecc"], }, "extra": { "payload": { "schema_version": "ai_route_repair_diagnosis_v1", "observed_state": { "target_resource": "ollama_gcp_a", "lane_mode": "degraded_failover", }, "live_probe": { "gcp_a_direct_22": "connection_refused", "gcp_a_direct_11434": "connection_refused", "gcp_b_direct_11434": "http_200", }, "access_blockers": [ "gcloud_compute_instances_get_missing", "gcp_a_ssh_refused", "gcp_a_ollama_11434_refused", ], "side_effects": { "incident_created": False, "telegram_sent": False, "approval_created": False, "runtime_route_changed": False, }, }, }, }, } item = _ai_route_repair_evidence_item(row) assert item["provider"] == "ai_route_repair" assert item["stage"] == "repair_diagnosis" assert item["target_resource"] == "ollama_gcp_a" assert item["run_id"] == "ca67ebcc-a24f-53e7-9505-2db15d855ecc" assert item["source_ref_count"] == 4 assert item["access_blockers"] == [ "gcloud_compute_instances_get_missing", "gcp_a_ssh_refused", "gcp_a_ollama_11434_refused", ] assert item["live_probe"]["gcp_a_direct_11434"] == "connection_refused" assert item["side_effects"] == { "incident_created": False, "telegram_sent": False, "approval_created": False, "runtime_route_changed": False, } assert item["work_item"] == { "schema_version": "awooop_ai_route_repair_work_item_v1", "work_item_id": "ai-route-repair:ollama_gcp_a", "status": "open", "kind": "ai_route_primary_lane_repair", "next_step": "restore_primary_ollama_lane_access", "reason": "primary_lane_unavailable", "needs_human": True, "owner": "cloud_sre_operator", "target_resource": "ollama_gcp_a", "target_href": "/awooop/runs", "decision_effect": "none", "safety_level": "read_only_work_item_projection", "writes_incident_state": False, "writes_auto_repair_result": False, "writes_runtime_route": False, } assert item["playbook_recommendation"]["playbook_id"] == ( "ai_route_primary_lane_recovery" ) assert item["playbook_recommendation"]["safe_to_auto_execute"] is False assert [step["step"] for step in item["playbook_recommendation"]["steps"]] == [ "verify_cloud_control_plane_access", "restore_gcp_a_os_access", "restore_ollama_service_on_gcp_a", "verify_ai_route_status_returns_primary", ] assert item["owner_action"]["lead_agent"] == "Hermes" assert item["owner_action"]["safe_to_auto_repair"] is False def test_ai_route_lane_state_marks_degraded_failover() -> None: policy = _ai_route_policy_order("deep_rca") health = { "ollama_gcp_a": { "status": "offline", "reason": "recent_endpoint_failure_cooldown:25s", }, "ollama_gcp_b": { "status": "healthy", "reason": "", }, "ollama_local": { "status": "healthy", "reason": "", }, } state = _ai_route_lane_state( policy_order=policy, selected_provider="ollama_gcp_b", health=health, ) assert state["lane_mode"] == "degraded_failover" assert state["active_lane"]["provider_name"] == "ollama_gcp_b" assert len(state["skipped_lanes"]) == 1 assert state["skipped_lanes"][0]["provider_name"] == "ollama_gcp_a" assert state["skipped_lanes"][0]["role"] == "primary" assert state["skipped_lanes"][0]["health_status"] == "offline" assert state["skipped_lanes"][0]["reason"] == "recent_endpoint_failure_cooldown:25s" assert state["skipped_lanes"][0]["action_required"] is True assert state["operator_action"] == { "human_required": True, "action": "repair_skipped_primary_lane", "reason": "fallback_lane_active", } def test_ai_route_lane_state_marks_cloud_fallback() -> None: policy = _ai_route_policy_order("deep_rca") health = { "ollama_gcp_a": {"status": "offline", "reason": "timeout"}, "ollama_gcp_b": {"status": "offline", "reason": "timeout"}, "ollama_local": {"status": "offline", "reason": "timeout"}, } state = _ai_route_lane_state( policy_order=policy, selected_provider="gemini", health=health, ) assert state["lane_mode"] == "cloud_fallback" assert state["active_lane"]["provider_name"] == "gemini" assert [lane["provider_name"] for lane in state["skipped_lanes"]] == [ "ollama_gcp_a", "ollama_gcp_b", "ollama_local", ] assert state["operator_action"]["action"] == "restore_ollama_lanes" @pytest.mark.asyncio async def test_ai_route_status_times_out_before_slow_provider_checks(monkeypatch) -> None: class SlowFailoverManager: async def select_provider(self, task_type: str = "general") -> None: await asyncio.sleep(0.05) async def fake_connectivity(endpoint): if endpoint.provider_name == "ollama_gcp_a": return HealthReport( status=HealthStatus.OFFLINE, host=endpoint.url, reason="timeout", ) return HealthReport( status=HealthStatus.HEALTHY, host=endpoint.url, latency_ms=12.3, reason="status_only_connectivity_ok", ) monkeypatch.setattr( platform_operator_service, "_AI_ROUTE_STATUS_SELECT_TIMEOUT_SECONDS", 0.001, ) monkeypatch.setattr( platform_operator_service, "get_ollama_failover_manager", lambda: SlowFailoverManager(), ) monkeypatch.setattr( platform_operator_service, "_ai_route_probe_connectivity", fake_connectivity, ) async def no_repair_evidence(**_kwargs): return None monkeypatch.setattr( platform_operator_service, "_latest_ai_route_repair_evidence", no_repair_evidence, ) response = await platform_operator_service.get_ai_route_status("deep_rca") assert response["route_reason"] == ( "route_check_timeout; lightweight connectivity selected ollama_gcp_b" ) assert response["route_error"] is None assert response["route_source"] == "lightweight_connectivity_fallback" assert response["selected_provider"] == "ollama_gcp_b" assert response["health"]["ollama_gcp_a"]["status"] == "offline" assert response["health"]["ollama_gcp_b"]["status"] == "healthy" assert [item["provider_name"] for item in response["fallback_chain"]] == [ "ollama_local", "gemini", ] assert [item["provider_name"] for item in response["policy_order"]] == [ "ollama_gcp_a", "ollama_gcp_b", "ollama_local", "gemini", ] @pytest.mark.asyncio async def test_ai_route_status_lightweight_fallback_keeps_gemini_policy_only( monkeypatch, ) -> None: class SlowFailoverManager: async def select_provider(self, task_type: str = "general") -> None: await asyncio.sleep(0.05) async def fake_offline_connectivity(endpoint): return HealthReport( status=HealthStatus.OFFLINE, host=endpoint.url, reason="offline", ) monkeypatch.setattr( platform_operator_service, "_AI_ROUTE_STATUS_SELECT_TIMEOUT_SECONDS", 0.001, ) monkeypatch.setattr( platform_operator_service, "get_ollama_failover_manager", lambda: SlowFailoverManager(), ) monkeypatch.setattr( platform_operator_service, "_ai_route_probe_connectivity", fake_offline_connectivity, ) async def no_repair_evidence(**_kwargs): return None monkeypatch.setattr( platform_operator_service, "_latest_ai_route_repair_evidence", no_repair_evidence, ) response = await platform_operator_service.get_ai_route_status("deep_rca") assert response["selected_provider"] == "gemini" assert response["selected_model"] is None assert response["route_source"] == "lightweight_connectivity_fallback" assert response["route_error"] is None assert "final fallback policy is Gemini" in response["route_reason"] assert all(item["status"] == "offline" for item in response["health"].values()) def test_ai_route_workload_validation_rejects_unknown_value() -> None: assert _validate_ai_route_workload(" hermes ") == "hermes" with pytest.raises(HTTPException) as exc_info: _validate_ai_route_workload("charge_money") assert "Unsupported workload_type" in str(exc_info.value.detail)