diff --git a/apps/api/src/services/ai_agent_autonomous_runtime_control.py b/apps/api/src/services/ai_agent_autonomous_runtime_control.py index e2d3be8d..6818e1f9 100644 --- a/apps/api/src/services/ai_agent_autonomous_runtime_control.py +++ b/apps/api/src/services/ai_agent_autonomous_runtime_control.py @@ -882,9 +882,21 @@ def _build_learning_loop_readback( and controlled_retry_package.get("schema_version") == "ai_agent_controlled_retry_package_v1" ) + learned_context_ready = bool( + verifier_total > 0 + and km_total > 0 + and learning_writeback_total > 0 + and trust_total > 0 + and learning_source_family_count > 0 + and repair_feedback_ready + ) next_decision_ready = bool( agent_decision_wiring.get("status") == "completed" - and loop_ledger.get("closed") is True + and ( + loop_ledger.get("closed") is True + or latest_flow_closure.get("closed") is True + or learned_context_ready + ) ) stages = [ _learning_loop_stage( @@ -1546,8 +1558,8 @@ def _build_work_item_progress( decision_wiring_missing = _int_value(decision_rollups.get("required_stage_missing_count")) p1a_completed = inactive_source_count == 0 p1b_completed = ( - p1a_completed - and agent_decision_wiring.get("schema_version") == "ai_agent_decision_wiring_readback_v1" + agent_decision_wiring.get("schema_version") == "ai_agent_decision_wiring_readback_v1" + and agent_decision_wiring.get("status") == "completed" and decision_wiring_missing == 0 ) learning_rollups = learning_loop.get("rollups") @@ -1555,8 +1567,8 @@ def _build_work_item_progress( learning_rollups = {} learning_loop_missing = _int_value(learning_rollups.get("required_stage_missing_count")) p1c_completed = ( - p1b_completed - and learning_loop.get("schema_version") == "ai_agent_learning_loop_readback_v1" + learning_loop.get("schema_version") == "ai_agent_learning_loop_readback_v1" + and learning_loop.get("status") == "completed" and learning_loop_missing == 0 ) alert_noise_rollups = alert_noise_reduction.get("rollups") @@ -1564,9 +1576,9 @@ def _build_work_item_progress( alert_noise_rollups = {} alert_noise_missing = _int_value(alert_noise_rollups.get("required_stage_missing_count")) p1d_completed = ( - p1c_completed - and alert_noise_reduction.get("schema_version") + alert_noise_reduction.get("schema_version") == "ai_agent_alert_noise_reduction_readback_v1" + and alert_noise_reduction.get("status") == "completed" and alert_noise_missing == 0 ) ui_rollups = ui_productization.get("rollups") @@ -1574,9 +1586,9 @@ def _build_work_item_progress( ui_rollups = {} ui_surface_missing = _int_value(ui_rollups.get("required_surface_missing_count")) p2a_completed = ( - p1d_completed - and ui_productization.get("schema_version") + ui_productization.get("schema_version") == "ai_agent_ui_productization_readback_v1" + and ui_productization.get("status") == "completed" and ui_surface_missing == 0 ) multi_product_rollups = multi_product_taxonomy.get("rollups") @@ -1588,9 +1600,9 @@ def _build_work_item_progress( else [] ) + _int_value(multi_product_rollups.get("missing_required_dimension_count")) p2b_completed = ( - p2a_completed - and multi_product_taxonomy.get("schema_version") + multi_product_taxonomy.get("schema_version") == "ai_agent_multi_product_taxonomy_contract_v1" + and multi_product_taxonomy.get("status") == "completed" and multi_product_missing == 0 ) deployed_readback_complete = ( diff --git a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py index 797b0f3b..880b1b28 100644 --- a/apps/api/tests/test_ai_agent_autonomous_runtime_control.py +++ b/apps/api/tests/test_ai_agent_autonomous_runtime_control.py @@ -582,6 +582,194 @@ def test_runtime_receipt_readback_summarizes_live_executor_closure_rows(): assert progress["rollups"]["pending_count"] == 0 +def test_runtime_receipt_work_items_use_learning_receipts_without_latest_telegram_gate(): + apply_op_id = "2f8ef5c8-fd4e-4950-99e9-dc9e61150cab" + incident_id = "INC-20260629-LEARNING" + + readback = build_runtime_receipt_readback_from_rows( + project_id="awoooi", + db_read_status="ok", + operation_count_rows=[ + { + "operation_type": "ansible_candidate_matched", + "status": "dry_run", + "total": 1, + "recent": 1, + }, + { + "operation_type": "ansible_check_mode_executed", + "status": "success", + "total": 1, + "recent": 1, + }, + { + "operation_type": "ansible_apply_executed", + "status": "success", + "total": 1, + "recent": 1, + }, + { + "operation_type": "ansible_learning_writeback_recorded", + "status": "success", + "total": 1, + "recent": 1, + }, + ], + operation_latest_rows=[ + { + "op_id": "candidate-op-learning", + "parent_op_id": None, + "operation_type": "ansible_candidate_matched", + "status": "dry_run", + "actor": "decision_manager", + "incident_id": incident_id, + "catalog_id": "ansible:188-ai-web", + "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", + "execution_mode": "check_mode", + }, + { + "op_id": "check-mode-op-learning", + "parent_op_id": "candidate-op-learning", + "operation_type": "ansible_check_mode_executed", + "status": "success", + "actor": "ansible_check_mode_worker", + "incident_id": incident_id, + "catalog_id": "ansible:188-ai-web", + "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", + "execution_mode": "check_mode", + "returncode": "0", + }, + { + "op_id": apply_op_id, + "parent_op_id": "check-mode-op-learning", + "operation_type": "ansible_apply_executed", + "status": "success", + "actor": "ansible_controlled_apply_worker", + "incident_id": incident_id, + "catalog_id": "ansible:188-ai-web", + "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", + "execution_mode": "controlled_apply", + "source_candidate_op_id": "candidate-op-learning", + "check_mode_op_id": "check-mode-op-learning", + "returncode": "0", + }, + { + "op_id": "learning-op", + "parent_op_id": apply_op_id, + "operation_type": "ansible_learning_writeback_recorded", + "status": "success", + "actor": "ansible_controlled_apply_worker", + "incident_id": incident_id, + "catalog_id": "ansible:188-ai-web", + "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", + "execution_mode": "learning_writeback", + "returncode": "0", + }, + ], + auto_repair_count_rows=[ + {"result_status": "success", "total": 1, "recent": 1}, + ], + auto_repair_latest_rows=[ + { + "id": "auto-repair-learning", + "incident_id": incident_id, + "catalog_id": "ansible:188-ai-web", + "playbook_name": "infra/ansible/playbooks/188-ai-web.yml", + "result_status": "success", + "executed_steps_text": f'["apply:{apply_op_id}"]', + "triggered_by": "ansible_controlled_apply", + "risk_level": "low", + "execution_time_ms": 2400, + }, + ], + verifier_count_rows=[ + {"verification_result": "success", "total": 1, "recent": 1}, + ], + verifier_latest_rows=[ + { + "id": "evidence-learning", + "incident_id": incident_id, + "verification_result": "success", + "apply_op_id": apply_op_id, + "catalog_id": "ansible:188-ai-web", + "playbook_path": "infra/ansible/playbooks/188-ai-web.yml", + "returncode": "0", + }, + ], + km_count_rows=[ + {"status": "review", "total": 1, "recent": 1}, + ], + km_latest_rows=[ + { + "id": "km-learning", + "title": "AI 自動修復沉澱:INC-20260629-LEARNING", + "related_incident_id": incident_id, + "related_playbook_id": "ansible:188-ai-web", + "path_type": "ansible_apply_receipt:2f8ef5c8", + "status": "review", + "created_by": "ai_agent_ansible_worker", + }, + ], + telegram_count_rows=[ + {"send_status": "sent", "total": 1, "recent": 1}, + ], + telegram_latest_rows=[], + mcp_gateway_count_rows=[ + {"status": "success", "total": 1, "recent": 1}, + ], + legacy_mcp_count_rows=[ + {"status": "success", "total": 1, "recent": 1}, + ], + service_log_count_rows=[ + {"status": "sanitized_recent_logs", "total": 1, "recent": 1}, + ], + executor_log_count_rows=[ + {"status": "success", "total": 1, "recent": 1}, + ], + timeline_count_rows=[ + {"status": "success", "total": 1, "recent": 1}, + ], + playbook_trust_count_rows=[ + {"status": "learning_active", "total": 1, "recent": 1}, + ], + alert_operation_count_rows=[ + {"event_type": "ALERT_RECEIVED", "total": 5, "recent": 2}, + {"event_type": "AUTO_REPAIR_TRIGGERED", "total": 2, "recent": 1}, + {"event_type": "EXECUTION_STARTED", "total": 1, "recent": 1}, + {"event_type": "EXECUTION_COMPLETED", "total": 1, "recent": 1}, + ], + alertmanager_event_count_rows=[ + {"stage": "received", "total": 5, "recent": 2}, + {"stage": "converged", "total": 3, "recent": 1}, + {"stage": "llm_inflight_suppressed", "total": 1, "recent": 1}, + ], + grouped_alert_event_count_rows=[ + {"status": "grouped_child_alert", "total": 4, "recent": 1}, + ], + ) + + assert readback["latest_flow_closure"]["closed"] is False + assert readback["latest_flow_closure"]["missing"] == ["telegram_receipt"] + assert readback["autonomous_execution_loop_ledger"]["closed"] is False + assert readback["learning_loop"]["status"] == "completed" + assert readback["learning_loop"]["missing_required_stage_ids"] == [] + assert readback["learning_loop"]["rollups"]["next_decision_ready_count"] == 1 + assert readback["alert_noise_reduction"]["status"] == "completed" + assert readback["alert_noise_reduction"]["missing_required_stage_ids"] == [] + progress = readback["work_item_progress"] + statuses = { + item["work_item_id"]: item["status"] + for item in progress["ordered_items"] + } + assert statuses["P1-C-learning-loop"] == "completed" + assert statuses["P1-D-alert-noise-reduction"] == "completed" + assert statuses["P2-A-ui-ux-productization"] == "completed" + assert statuses["P2-B-multi-product-expansion"] == "completed" + assert {item["status"] for item in progress["source_family_items"]} == {"completed"} + assert progress["rollups"]["completed_count"] == 21 + assert progress["rollups"]["pending_count"] == 0 + + def test_runtime_receipt_readback_classifies_closed_failed_apply_as_ai_repair(): apply_op_id = "94925d5e-6fdc-49c3-90e8-f0a0d57a6a58" incident_id = "INC-20260628-A40A9A"