diff --git a/apps/api/src/services/gitea_cicd_alert_receipt_chain_readback.py b/apps/api/src/services/gitea_cicd_alert_receipt_chain_readback.py index f92170e67..e6ca4f7c1 100644 --- a/apps/api/src/services/gitea_cicd_alert_receipt_chain_readback.py +++ b/apps/api/src/services/gitea_cicd_alert_receipt_chain_readback.py @@ -60,11 +60,28 @@ def build_gitea_cicd_alert_receipt_chain_readback( monitoring_gap_count = _int(coverage_summary.get("monitoring_live_receipt_gap_count")) full_dr_gap_count = _int(full_server_backup_dr_receipts.get("active_gap_count")) + full_dr_gap_ids = _list(full_server_backup_dr_receipts.get("active_gap_ids")) + full_dr_restore_drill_only = ( + full_dr_gap_ids == ["restore_drill_approval_gate"] + and _int(full_dr_summary.get("source_ready_component_count")) >= 6 + and _int(full_dr_summary.get("receipt_required_component_count")) == 0 + ) quiet_contract_count = len(_list(backup_rollups.get("suppressed_success_rule_ids"))) immediate_escalation_count = len( _list(backup_rollups.get("immediate_escalation_rule_ids")) ) notify_workflow_count = _int(workflow_rollups.get("workflows_with_notify_bridge")) + ai_context_source_ready = ( + coverage_summary.get("source_contract_ready") is True + and _int(coverage_summary.get("alert_receipt_pipeline_ready_count")) > 0 + ) + ai_context_verified_ready = ( + ( + coverage_summary.get("ai_loop_context_ready") is True + and _int(coverage_summary.get("ai_loop_ai_agent_context_receipt_count")) > 0 + ) + or _int(coverage_summary.get("verified_ai_agent_context_receipt_count")) > 0 + ) receipt_rows = [ { @@ -95,17 +112,30 @@ def build_gitea_cicd_alert_receipt_chain_readback( { "chain_id": "gitea_full_server_dr_receipts", "domain": "backup_dr", - "status": "action_required" if full_dr_gap_count else "verified_ready", - "receipt_state": "component_receipts_pending" - if full_dr_gap_count - else "component_receipts_ready", + "status": "verified_ready" + if not full_dr_gap_count + else "controlled_restore_drill_pending" + if full_dr_restore_drill_only + else "action_required", + "receipt_state": "component_receipts_ready" + if not full_dr_gap_count + else "source_contract_ready_restore_drill_pending" + if full_dr_restore_drill_only + else "component_receipts_pending", "evidence_refs": ["GET /api/v1/agents/gitea-full-server-backup-dr-receipts"], - "next_action": "close full-server component receipt rows before DR complete", + "next_action": "run a controlled restore-drill verifier before DR complete" + if full_dr_restore_drill_only + else "close full-server component receipt rows before DR complete", "metrics": { "full_server_active_gap_count": full_dr_gap_count, + "full_server_active_gap_ids": full_dr_gap_ids, "full_server_verified_ready_component_count": _int( full_dr_summary.get("verified_ready_component_count") ), + "full_server_source_ready_component_count": _int( + full_dr_summary.get("source_ready_component_count") + ), + "restore_drill_only_gap": full_dr_restore_drill_only, }, }, { @@ -132,14 +162,40 @@ def build_gitea_cicd_alert_receipt_chain_readback( { "chain_id": "ai_agent_context_receipts", "domain": "ai_context", - "status": "source_ready" - if _int(coverage_summary.get("verified_ai_agent_context_receipt_count")) > 0 + "status": "verified_ready" + if ai_context_verified_ready + else "source_ready" + if ai_context_source_ready else "action_required", - "receipt_state": "ai_context_receipts_projected", + "receipt_state": "ai_context_receipts_verified" + if ai_context_verified_ready + else "source_backed_ai_context_pipeline_ready" + if ai_context_source_ready + else "ai_context_receipts_projected", "evidence_refs": [ "GET /api/v1/agents/telegram-alert-monitoring-coverage-readback" ], - "next_action": "keep KM/RAG/MCP/PlayBook context receipts metadata-only", + "next_action": "keep KM/RAG/MCP/PlayBook context receipts metadata-only" + if ai_context_source_ready + else "write metadata-only KM/RAG/MCP/PlayBook context receipts", + "metrics": { + "source_contract_ready": coverage_summary.get("source_contract_ready") + is True, + "ai_loop_context_ready": coverage_summary.get("ai_loop_context_ready") + is True, + "ai_loop_context_source": str( + coverage_summary.get("ai_loop_context_source") or "" + ), + "ai_loop_ai_agent_context_receipt_count": _int( + coverage_summary.get("ai_loop_ai_agent_context_receipt_count") + ), + "verified_ai_agent_context_receipt_count": _int( + coverage_summary.get("verified_ai_agent_context_receipt_count") + ), + "alert_receipt_pipeline_ready_count": _int( + coverage_summary.get("alert_receipt_pipeline_ready_count") + ), + }, }, ] @@ -181,9 +237,20 @@ def build_gitea_cicd_alert_receipt_chain_readback( "source_or_policy_ready_chain_count": sum( 1 for row in receipt_rows - if row["status"] in {"source_ready", "policy_ready", "verified_ready"} + if row["status"] + in { + "source_ready", + "policy_ready", + "verified_ready", + "controlled_restore_drill_pending", + } ), "action_required_chain_count": len(active_gaps), + "controlled_restore_drill_pending_chain_count": sum( + 1 + for row in receipt_rows + if row["status"] == "controlled_restore_drill_pending" + ), "monitoring_live_receipt_gap_count": monitoring_gap_count, "full_server_dr_active_gap_count": full_dr_gap_count, "workflow_notify_bridge_count": notify_workflow_count, diff --git a/apps/api/tests/test_gitea_cicd_alert_receipt_chain_readback_api.py b/apps/api/tests/test_gitea_cicd_alert_receipt_chain_readback_api.py index 435009b82..56c6a1f79 100644 --- a/apps/api/tests/test_gitea_cicd_alert_receipt_chain_readback_api.py +++ b/apps/api/tests/test_gitea_cicd_alert_receipt_chain_readback_api.py @@ -73,7 +73,51 @@ def test_gitea_cicd_alert_receipt_chain_readback_surfaces_receipt_gaps() -> None assert by_id["backup_notification_policy"]["status"] == "policy_ready" assert by_id["gitea_full_server_dr_receipts"]["status"] == "action_required" assert by_id["telegram_monitoring_live_receipts"]["status"] == "action_required" - assert by_id["ai_agent_context_receipts"]["status"] == "source_ready" + assert by_id["ai_agent_context_receipts"]["status"] == "verified_ready" + + +def test_gitea_cicd_alert_receipt_chain_keeps_restore_drill_pending_separate() -> None: + full_dr = { + "schema_version": "gitea_full_server_backup_dr_receipts_v1", + "status": "gitea_full_server_backup_dr_receipts_gaps_present", + "active_gap_count": 1, + "active_gap_ids": ["restore_drill_approval_gate"], + "summary": { + "verified_ready_component_count": 3, + "source_ready_component_count": 6, + "receipt_required_component_count": 0, + }, + } + telegram_coverage = { + **_telegram_coverage(), + "summary": { + **_telegram_coverage()["summary"], + "source_contract_ready": True, + "ai_loop_context_ready": False, + "ai_loop_ai_agent_context_receipt_count": 0, + }, + } + + payload = build_gitea_cicd_alert_receipt_chain_readback( + telegram_coverage=telegram_coverage, + backup_notification_policy=load_latest_backup_notification_policy(), + workflow_runner_health=load_latest_gitea_workflow_runner_health(), + full_server_backup_dr_receipts=full_dr, + generated_at="2026-07-10T00:21:00+08:00", + ) + + assert payload["summary"]["action_required_chain_count"] == 1 + assert payload["summary"]["controlled_restore_drill_pending_chain_count"] == 1 + assert payload["active_gap_ids"] == ["telegram_monitoring_live_receipts"] + + by_id = {row["chain_id"]: row for row in payload["receipt_chain_rows"]} + assert by_id["gitea_full_server_dr_receipts"]["status"] == ( + "controlled_restore_drill_pending" + ) + assert by_id["gitea_full_server_dr_receipts"]["metrics"][ + "restore_drill_only_gap" + ] is True + assert by_id["ai_agent_context_receipts"]["status"] == "verified_ready" @pytest.mark.asyncio