fix(api): distinguish ansible dry run from repair
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m35s
CD Pipeline / build-and-deploy (push) Successful in 5m0s
CD Pipeline / post-deploy-checks (push) Successful in 1m33s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m35s
CD Pipeline / build-and-deploy (push) Successful in 5m0s
CD Pipeline / post-deploy-checks (push) Successful in 1m33s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -1732,6 +1732,82 @@ def test_awooop_status_chain_surfaces_controlled_ansible_apply_proof() -> None:
|
||||
assert ansible["approval_source"] == "user_chat_approved_continue"
|
||||
|
||||
|
||||
def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> None:
|
||||
chain = _build_awooop_status_chain(
|
||||
incident_ids=["INC-20260625-977E5F"],
|
||||
source_id="INC-20260625-977E5F",
|
||||
truth_chain={
|
||||
"truth_status": {
|
||||
"current_stage": "execution_succeeded",
|
||||
"stage_status": "success",
|
||||
"needs_human": True,
|
||||
"blockers": ["incident_open_after_successful_execution"],
|
||||
},
|
||||
"automation_quality": {
|
||||
"verdict": "execution_unverified",
|
||||
"facts": {
|
||||
"auto_repair_execution_records": 0,
|
||||
"automation_operation_records": 2,
|
||||
"effective_execution_records": 1,
|
||||
"verification_result": None,
|
||||
"mcp_gateway_total": 8,
|
||||
"knowledge_entries": 0,
|
||||
},
|
||||
"blockers": ["verification_recorded"],
|
||||
"operator_outcome": {
|
||||
"state": "execution_unverified_manual_required",
|
||||
"needs_human": True,
|
||||
"next_action": "run_or_review_post_execution_verification",
|
||||
},
|
||||
},
|
||||
"execution": {
|
||||
"automation_operation_log": [
|
||||
{
|
||||
"operation_type": "ansible_check_mode_executed",
|
||||
"status": "success",
|
||||
"actor": "ansible_check_mode_worker",
|
||||
"input_executor": "ansible",
|
||||
"input_catalog_id": "ansible:188-ai-web",
|
||||
"input_playbook_path": "infra/ansible/playbooks/188-ai-web-readonly.yml",
|
||||
}
|
||||
],
|
||||
"ansible": {
|
||||
"considered": True,
|
||||
"records": [
|
||||
{
|
||||
"operation_type": "ansible_check_mode_executed",
|
||||
"status": "success",
|
||||
"actor": "ansible_check_mode_worker",
|
||||
"catalog_id": "ansible:188-ai-web",
|
||||
"playbook_path": "infra/ansible/playbooks/188-ai-web-readonly.yml",
|
||||
"execution_mode": "check_mode",
|
||||
"check_mode": True,
|
||||
"apply_executed": False,
|
||||
"returncode": 0,
|
||||
}
|
||||
],
|
||||
"candidate_catalog": {"candidates": []},
|
||||
},
|
||||
},
|
||||
},
|
||||
remediation_history={"total": 0},
|
||||
)
|
||||
|
||||
assert chain["verdict"] == "ansible_check_mode_only"
|
||||
assert chain["repair_state"] == "ansible_check_mode_only"
|
||||
assert chain["next_step"] == "owner_review_apply_gate_or_create_verifier_plan"
|
||||
assert chain["needs_human"] is True
|
||||
assert chain["evidence"]["ansible_dry_run_only"] is True
|
||||
assert chain["operator_outcome"]["state"] == "dry_run_only_owner_review_required"
|
||||
assert (
|
||||
chain["operator_outcome"]["execution_result"]["completion_status"]
|
||||
== "dry_run_completed_no_apply"
|
||||
)
|
||||
assert chain["execution"]["ansible"]["check_mode_total"] == 1
|
||||
assert chain["execution"]["ansible"]["apply_total"] == 0
|
||||
assert chain["execution"]["ansible"]["applied"] is False
|
||||
|
||||
|
||||
def test_awooop_status_chain_includes_source_provider_correlation() -> None:
|
||||
chain = _build_awooop_status_chain(
|
||||
incident_ids=["INC-20260520-4D1124"],
|
||||
|
||||
@@ -66,6 +66,35 @@ def test_operator_outcome_marks_unverified_execution_as_human_review() -> None:
|
||||
assert outcome["next_action"] == "run_or_review_post_execution_verification"
|
||||
|
||||
|
||||
def test_operator_outcome_marks_ansible_check_mode_as_dry_run_only() -> None:
|
||||
outcome = build_operator_outcome(
|
||||
truth_status={
|
||||
"current_stage": "execution_succeeded",
|
||||
"stage_status": "success",
|
||||
"needs_human": False,
|
||||
"blockers": [],
|
||||
},
|
||||
automation_quality={
|
||||
"verdict": "ansible_check_mode_only",
|
||||
"facts": {
|
||||
"effective_execution_records": 1,
|
||||
"auto_repair_execution_records": 0,
|
||||
"ansible_check_mode_total": 1,
|
||||
"ansible_apply_total": 0,
|
||||
"verification_result": None,
|
||||
},
|
||||
"blockers": [],
|
||||
},
|
||||
)
|
||||
|
||||
assert outcome["state"] == "dry_run_only_owner_review_required"
|
||||
assert outcome["needs_human"] is True
|
||||
assert outcome["next_action"] == "owner_review_apply_gate_or_create_verifier_plan"
|
||||
assert outcome["execution_result"]["completion_status"] == "dry_run_completed_no_apply"
|
||||
assert outcome["execution_result"]["command_status"] == "check_mode_succeeded"
|
||||
assert outcome["execution_result"]["repair_status"] == "not_executed"
|
||||
|
||||
|
||||
def test_operator_outcome_marks_verified_repair_as_result_only() -> None:
|
||||
outcome = build_operator_outcome(
|
||||
truth_status={
|
||||
|
||||
@@ -708,6 +708,74 @@ def test_awooop_status_chain_lines_do_not_treat_audit_ops_as_repair() -> None:
|
||||
assert "通知:<code>telegram_sre_war_room,awooop_operator_console</code>" in joined
|
||||
|
||||
|
||||
def test_awooop_status_chain_lines_mark_ansible_check_mode_as_dry_run_only() -> None:
|
||||
lines = telegram_gateway_module._format_awooop_status_chain_lines(
|
||||
truth_chain={
|
||||
"truth_status": {
|
||||
"current_stage": "execution_succeeded",
|
||||
"stage_status": "success",
|
||||
"needs_human": True,
|
||||
"blockers": ["incident_open_after_successful_execution"],
|
||||
},
|
||||
"automation_quality": {
|
||||
"verdict": "execution_unverified",
|
||||
"facts": {
|
||||
"auto_repair_execution_records": 0,
|
||||
"automation_operation_records": 2,
|
||||
"effective_execution_records": 1,
|
||||
"verification_result": None,
|
||||
"mcp_gateway_total": 8,
|
||||
"knowledge_entries": 0,
|
||||
},
|
||||
"blockers": ["verification_recorded"],
|
||||
"operator_outcome": {
|
||||
"state": "execution_unverified_manual_required",
|
||||
"needs_human": True,
|
||||
"next_action": "run_or_review_post_execution_verification",
|
||||
},
|
||||
},
|
||||
"execution": {
|
||||
"automation_operation_log": [
|
||||
{
|
||||
"operation_type": "ansible_check_mode_executed",
|
||||
"status": "success",
|
||||
"actor": "ansible_check_mode_worker",
|
||||
"input_executor": "ansible",
|
||||
"input_catalog_id": "ansible:188-ai-web",
|
||||
}
|
||||
],
|
||||
"ansible": {
|
||||
"considered": True,
|
||||
"records": [
|
||||
{
|
||||
"operation_type": "ansible_check_mode_executed",
|
||||
"status": "success",
|
||||
"actor": "ansible_check_mode_worker",
|
||||
"catalog_id": "ansible:188-ai-web",
|
||||
"playbook_path": "infra/ansible/playbooks/188-ai-web-readonly.yml",
|
||||
"execution_mode": "check_mode",
|
||||
"check_mode": True,
|
||||
"apply_executed": False,
|
||||
"returncode": 0,
|
||||
}
|
||||
],
|
||||
"candidate_catalog": {"candidates": []},
|
||||
},
|
||||
},
|
||||
},
|
||||
remediation_history={"total": 0},
|
||||
)
|
||||
|
||||
joined = "\n".join(lines)
|
||||
assert "ansible_check_mode_only" in joined
|
||||
assert "owner_review_apply_gate_or_create_verifier_plan" in joined
|
||||
assert "dry_run_only_owner_review_required" in joined
|
||||
assert "dry_run_completed_no_apply" in joined
|
||||
assert "只完成 Ansible check-mode 乾跑,尚未執行修復" in joined
|
||||
assert "executed_pending_verification" not in joined
|
||||
assert "run_or_review_post_execution_verification" not in joined
|
||||
|
||||
|
||||
def test_awooop_agent_evidence_lines_show_mcp_source_execution_playbook_km() -> None:
|
||||
"""Telegram 詳情/歷史要像前端一樣顯示五段 AI Agent 證據鏈。"""
|
||||
lines = telegram_gateway_module._format_awooop_agent_evidence_lines(
|
||||
|
||||
Reference in New Issue
Block a user