feat(awooop): enable controlled agent apply
Some checks failed
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / tests (push) Failing after 1m37s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / tests (push) Failing after 1m37s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
This commit is contained in:
@@ -145,22 +145,42 @@ class TestAutoRepairService:
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_evaluate_blocks_p1_severity(self, service):
|
||||
"""Test that P1 severity incidents are blocked"""
|
||||
async def test_evaluate_allows_p1_severity_with_approved_playbook(
|
||||
self,
|
||||
service,
|
||||
mock_playbook_service,
|
||||
):
|
||||
"""P1 severity no longer blocks approved LOW/MEDIUM/HIGH controlled repair."""
|
||||
playbook = create_high_quality_playbook(risk_level=RiskLevel.HIGH)
|
||||
mock_playbook_service.add_playbook(playbook)
|
||||
mock_playbook_service.set_recommendations([
|
||||
MockPlaybookRecommendation(playbook, similarity_score=0.9)
|
||||
])
|
||||
incident = create_test_incident(severity=Severity.P1)
|
||||
decision = await service.evaluate_auto_repair(incident)
|
||||
|
||||
assert decision.can_auto_repair is False
|
||||
assert decision.blocked_by == "HIGH_SEVERITY"
|
||||
assert decision.can_auto_repair is True
|
||||
assert decision.risk_level == RiskLevel.HIGH
|
||||
assert decision.blocked_by is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_evaluate_blocks_p0_severity(self, service):
|
||||
"""Test that P0 severity incidents are blocked"""
|
||||
async def test_evaluate_allows_p0_severity_with_approved_playbook(
|
||||
self,
|
||||
service,
|
||||
mock_playbook_service,
|
||||
):
|
||||
"""P0 severity is handled by PlayBook/Registry guardrails, not a blanket human gate."""
|
||||
playbook = create_high_quality_playbook(risk_level=RiskLevel.MEDIUM)
|
||||
mock_playbook_service.add_playbook(playbook)
|
||||
mock_playbook_service.set_recommendations([
|
||||
MockPlaybookRecommendation(playbook, similarity_score=0.9)
|
||||
])
|
||||
incident = create_test_incident(severity=Severity.P0)
|
||||
decision = await service.evaluate_auto_repair(incident)
|
||||
|
||||
assert decision.can_auto_repair is False
|
||||
assert decision.blocked_by == "HIGH_SEVERITY"
|
||||
assert decision.can_auto_repair is True
|
||||
assert decision.risk_level == RiskLevel.MEDIUM
|
||||
assert decision.blocked_by is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_evaluate_no_playbook_match(self, service, mock_playbook_service):
|
||||
@@ -252,10 +272,7 @@ class TestAutoRepairService:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_evaluate_critical_risk_blocked(self, service, mock_playbook_service):
|
||||
"""Test CRITICAL risk playbook is now approved (gates removed 2026-04-07).
|
||||
2026-04-07: 統帥指令移除風險等級門檻。
|
||||
2026-04-08 Claude Sonnet 4.6: 更新測試預期以符合當前設計。
|
||||
"""
|
||||
"""CRITICAL remains break-glass and is outside LOW/MEDIUM/HIGH authorization."""
|
||||
playbook = create_high_quality_playbook(risk_level=RiskLevel.CRITICAL)
|
||||
mock_playbook_service.add_playbook(playbook)
|
||||
mock_playbook_service.set_recommendations([
|
||||
@@ -265,9 +282,8 @@ class TestAutoRepairService:
|
||||
incident = create_test_incident(severity=Severity.P2)
|
||||
decision = await service.evaluate_auto_repair(incident)
|
||||
|
||||
# 風險等級門檻已移除 — CRITICAL risk APPROVED Playbook 也通過
|
||||
assert decision.can_auto_repair is True
|
||||
assert decision.blocked_by is None
|
||||
assert decision.can_auto_repair is False
|
||||
assert decision.blocked_by == "CRITICAL_BREAK_GLASS"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_evaluate_success(self, service, mock_playbook_service):
|
||||
|
||||
@@ -240,14 +240,14 @@ def test_cicd_event_filter_validation_and_duration_safety() -> None:
|
||||
_validate_cicd_status_filter("ignored")
|
||||
|
||||
|
||||
def test_outbound_timeline_title_labels_auto_repair_handoff() -> None:
|
||||
def test_outbound_timeline_title_labels_auto_repair_retry_queue() -> None:
|
||||
title = _outbound_timeline_title(
|
||||
"telegram",
|
||||
"error",
|
||||
"🤖❌ HANDOFF REQUIRED|AI 自動修復失敗,已轉人工",
|
||||
"🔁 AI RETRY QUEUED|AI 自動修復失敗,已排入下一輪修復",
|
||||
)
|
||||
|
||||
assert title == "TELEGRAM:AI 自動修復失敗,已轉人工"
|
||||
assert title == "TELEGRAM:AI 自動修復失敗,已排入重試"
|
||||
|
||||
|
||||
def test_outbound_timeline_title_falls_back_to_human_label() -> None:
|
||||
@@ -933,9 +933,10 @@ def test_ai_alert_card_delivery_item_uses_metadata_without_raw_content() -> None
|
||||
"event_type": "wazuh_dashboard_api_readback_degraded",
|
||||
"lane": "siem_observability_readback_degraded",
|
||||
"target": "wazuh_dashboard_api",
|
||||
"gates": ["candidate_only", "runtime_write_gate=0"],
|
||||
"candidate_only": True,
|
||||
"runtime_write_gate_count": 0,
|
||||
"gates": ["controlled_playbook_queue", "runtime_write_gate=controlled"],
|
||||
"candidate_only": False,
|
||||
"controlled_playbook_queue": True,
|
||||
"runtime_write_gate_count": 1,
|
||||
"delivery_receipt_readback_required": True,
|
||||
},
|
||||
"source_refs": {
|
||||
@@ -952,8 +953,8 @@ def test_ai_alert_card_delivery_item_uses_metadata_without_raw_content() -> None
|
||||
assert item["event_type"] == "wazuh_dashboard_api_readback_degraded"
|
||||
assert item["lane"] == "siem_observability_readback_degraded"
|
||||
assert item["target"] == "wazuh_dashboard_api"
|
||||
assert item["runtime_write_gate_count"] == 0
|
||||
assert item["runtime_write_allowed"] is False
|
||||
assert item["runtime_write_gate_count"] == 1
|
||||
assert item["runtime_write_allowed"] is True
|
||||
assert item["delivery_receipt_readback_required"] is True
|
||||
assert item["source_refs"]["alert_ids"] == [
|
||||
"wazuh_dashboard_api_readback_degraded"
|
||||
@@ -972,7 +973,7 @@ def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None:
|
||||
"pending_total": 0,
|
||||
"shadow_total": 0,
|
||||
"delivery_receipt_required_total": 2,
|
||||
"runtime_write_gate_open_count": 0,
|
||||
"runtime_write_gate_open_count": 1,
|
||||
"latest_sent_at": datetime(2026, 6, 25, 9, 40, 5),
|
||||
"latest_queued_at": datetime(2026, 6, 25, 9, 40, 0),
|
||||
},
|
||||
@@ -984,8 +985,8 @@ def test_ai_alert_card_delivery_summary_keeps_no_false_green_status() -> None:
|
||||
assert summary["schema_version"] == "awooop_ai_alert_card_delivery_readback_v1"
|
||||
assert summary["status"] == "delivery_failure_observed"
|
||||
assert summary["delivery_receipt_required_total"] == 2
|
||||
assert summary["runtime_write_gate_open_count"] == 0
|
||||
assert summary["runtime_write_allowed"] is False
|
||||
assert summary["runtime_write_gate_open_count"] == 1
|
||||
assert summary["runtime_write_allowed"] is True
|
||||
assert summary["production_write_count"] == 0
|
||||
|
||||
|
||||
@@ -1008,10 +1009,11 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None:
|
||||
"event_type": "wazuh_dashboard_api_readback_degraded",
|
||||
"lane": "siem_observability_readback_degraded",
|
||||
"target": "wazuh_dashboard_api",
|
||||
"gates": ["candidate_only", "runtime_write_gate=0"],
|
||||
"runtime_write_gate_count": 0,
|
||||
"runtime_write_allowed": False,
|
||||
"candidate_only": True,
|
||||
"gates": ["controlled_playbook_queue", "runtime_write_gate=controlled"],
|
||||
"runtime_write_gate_count": 1,
|
||||
"runtime_write_allowed": True,
|
||||
"candidate_only": False,
|
||||
"controlled_playbook_queue": True,
|
||||
"delivery_receipt_readback_required": True,
|
||||
"source_refs": {
|
||||
"alert_ids": ["wazuh_dashboard_api_readback_degraded"],
|
||||
@@ -1040,8 +1042,8 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None:
|
||||
"pending_total": 0,
|
||||
"shadow_total": 0,
|
||||
"delivery_receipt_required_total": 1,
|
||||
"runtime_write_gate_open_count": 0,
|
||||
"runtime_write_allowed": False,
|
||||
"runtime_write_gate_open_count": 1,
|
||||
"runtime_write_allowed": True,
|
||||
"latest_sent_at": datetime(2026, 6, 25, 9, 40, 5),
|
||||
"latest_queued_at": datetime(2026, 6, 25, 9, 40, 0),
|
||||
"production_write_count": 0,
|
||||
@@ -1052,7 +1054,7 @@ def test_list_ai_alert_cards_response_preserves_delivery_metadata() -> None:
|
||||
assert dumped["items"][0]["event_type"] == (
|
||||
"wazuh_dashboard_api_readback_degraded"
|
||||
)
|
||||
assert dumped["items"][0]["runtime_write_allowed"] is False
|
||||
assert dumped["items"][0]["runtime_write_allowed"] is True
|
||||
assert dumped["summary"]["delivery_receipt_required_total"] == 1
|
||||
assert dumped["summary"]["production_write_count"] == 0
|
||||
|
||||
@@ -1795,24 +1797,24 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
)
|
||||
|
||||
assert chain["verdict"] == "ansible_check_mode_only"
|
||||
assert chain["repair_state"] == "ansible_check_mode_only"
|
||||
assert chain["next_step"] == "open_apply_gate_work_item_review_verifier_and_km"
|
||||
assert chain["needs_human"] is True
|
||||
assert chain["repair_state"] == "controlled_apply_queued"
|
||||
assert chain["next_step"] == "wait_for_controlled_apply_and_post_apply_verifier"
|
||||
assert chain["needs_human"] is False
|
||||
assert chain["evidence"]["ansible_dry_run_only"] is True
|
||||
assert chain["operator_outcome"]["state"] == "apply_candidate_owner_review_ready"
|
||||
assert "apply candidate" in chain["operator_outcome"]["summary_zh"]
|
||||
assert chain["operator_outcome"]["state"] == "controlled_apply_queued"
|
||||
assert "受控自動 apply" in chain["operator_outcome"]["summary_zh"]
|
||||
assert (
|
||||
chain["operator_outcome"]["execution_result"]["completion_status"]
|
||||
== "dry_run_passed_apply_candidate_ready"
|
||||
== "dry_run_passed_controlled_apply_queued"
|
||||
)
|
||||
assert "verification_missing" in chain["blockers"]
|
||||
assert "verification_recorded" not in chain["blockers"]
|
||||
assert chain["automation_handoff"]["kind"] == "ansible_check_mode_apply_gate"
|
||||
assert chain["automation_handoff"]["status"] == "owner_review_required"
|
||||
assert chain["automation_handoff"]["status"] == "controlled_apply_auto_authorized"
|
||||
assert chain["automation_handoff"]["next_action"] == (
|
||||
"open_apply_gate_work_item_review_verifier_and_km"
|
||||
"wait_for_controlled_apply_worker_and_verifier"
|
||||
)
|
||||
assert chain["automation_handoff"]["runtime_execution_authorized"] is False
|
||||
assert chain["automation_handoff"]["runtime_execution_authorized"] is True
|
||||
assert chain["automation_handoff"]["work_item_id"].startswith(
|
||||
"ansible-apply-gate:awoooi:INC-20260625-977E5F"
|
||||
)
|
||||
@@ -1824,19 +1826,19 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
)
|
||||
assert [gate["status"] for gate in chain["automation_handoff"]["gates"]] == [
|
||||
"passed",
|
||||
"blocked",
|
||||
"controlled_apply_authorized",
|
||||
"blocked",
|
||||
]
|
||||
closure = chain["automation_handoff"]["closure_readiness"]
|
||||
assert closure["schema_version"] == "awooop_apply_gate_closure_readiness_v1"
|
||||
assert closure["status"] == "blocked_before_owner_release"
|
||||
assert closure["runtime_execution_authorized"] is False
|
||||
assert closure["writes_runtime_state"] is False
|
||||
assert closure["ready_count"] == 3
|
||||
assert closure["status"] == "controlled_apply_auto_authorized"
|
||||
assert closure["runtime_execution_authorized"] is True
|
||||
assert closure["writes_runtime_state"] is True
|
||||
assert closure["ready_count"] == 8
|
||||
assert closure["total_count"] == 8
|
||||
assert closure["blocked_count"] == 5
|
||||
assert closure["completion_percent"] == 38
|
||||
assert closure["next_action"] == "review_owner_release_packet_before_apply"
|
||||
assert closure["blocked_count"] == 0
|
||||
assert closure["completion_percent"] == 100
|
||||
assert closure["next_action"] == "controlled_apply_worker_executes"
|
||||
assert [gate["key"] for gate in closure["gates"]] == [
|
||||
"mcp_evidence",
|
||||
"dry_run",
|
||||
@@ -1851,15 +1853,13 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
"passed",
|
||||
"passed",
|
||||
"passed",
|
||||
"blocked",
|
||||
"blocked",
|
||||
"blocked",
|
||||
"blocked",
|
||||
"blocked",
|
||||
"auto_waived_by_owner_policy",
|
||||
"controlled_apply_authorized",
|
||||
"queued_after_controlled_apply",
|
||||
"queued_after_verifier",
|
||||
"queued_after_verifier",
|
||||
]
|
||||
assert "owner_approval_receipt" in closure["required_owner_fields"]
|
||||
assert "post_apply_verifier_plan" in closure["required_owner_fields"]
|
||||
assert "km_writeback_owner" in closure["required_owner_fields"]
|
||||
assert closure["required_owner_fields"] == []
|
||||
assert closure["readback_assets"][0]["asset_id"] == (
|
||||
"agent-result-capture-owner-approved-execution-rehearsal:P2-126"
|
||||
)
|
||||
@@ -1895,7 +1895,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
"post_apply_verifier_preflight",
|
||||
"km_playbook_trust_writeback_plan",
|
||||
]
|
||||
assert {task["runtime_write_allowed"] for task in closure["closure_tasks"]} == {False}
|
||||
assert {task["runtime_write_allowed"] for task in closure["closure_tasks"]} == {True}
|
||||
assert closure["closure_tasks"][0]["source_asset_id"] == (
|
||||
"agent-result-capture-owner-release-approval-gate:P2-131"
|
||||
)
|
||||
@@ -1906,14 +1906,14 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
assert controlled_execution["schema_version"] == (
|
||||
"awooop_controlled_execution_preflight_v1"
|
||||
)
|
||||
assert controlled_execution["status"] == "blocked_before_runtime_gate"
|
||||
assert controlled_execution["runtime_execution_authorized"] is False
|
||||
assert controlled_execution["runtime_write_allowed"] is False
|
||||
assert controlled_execution["status"] == "controlled_apply_allowed"
|
||||
assert controlled_execution["runtime_execution_authorized"] is True
|
||||
assert controlled_execution["runtime_write_allowed"] is True
|
||||
assert controlled_execution["candidate_route_count"] == 1
|
||||
assert controlled_execution["allowed_route_count"] == 0
|
||||
assert controlled_execution["ready_count"] == 2
|
||||
assert controlled_execution["allowed_route_count"] == 1
|
||||
assert controlled_execution["ready_count"] == 5
|
||||
assert controlled_execution["total_count"] == 7
|
||||
assert controlled_execution["blocked_count"] == 5
|
||||
assert controlled_execution["blocked_count"] == 2
|
||||
assert [item["key"] for item in controlled_execution["prerequisites"]] == [
|
||||
"dry_run_passed",
|
||||
"allowlisted_route_candidate",
|
||||
@@ -1926,7 +1926,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
assert controlled_execution["routes"][0]["route_id"] == (
|
||||
"ansible-allowlisted-apply:ansible:188-ai-web"
|
||||
)
|
||||
assert controlled_execution["routes"][0]["allowed"] is False
|
||||
assert controlled_execution["routes"][0]["allowed"] is True
|
||||
assert controlled_execution["routes"][0]["apply_playbook_path"] == (
|
||||
"infra/ansible/playbooks/188-ai-web.yml"
|
||||
)
|
||||
@@ -1934,15 +1934,15 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
assert release_contract["schema_version"] == (
|
||||
"awooop_execution_release_contract_v1"
|
||||
)
|
||||
assert release_contract["status"] == "draft_prefilled_needs_owner_release"
|
||||
assert release_contract["runtime_execution_authorized"] is False
|
||||
assert release_contract["runtime_write_allowed"] is False
|
||||
assert release_contract["status"] == "controlled_apply_auto_authorized"
|
||||
assert release_contract["runtime_execution_authorized"] is True
|
||||
assert release_contract["runtime_write_allowed"] is True
|
||||
assert release_contract["route_id"] == (
|
||||
"ansible-allowlisted-apply:ansible:188-ai-web"
|
||||
)
|
||||
assert release_contract["ready_count"] == 10
|
||||
assert release_contract["ready_count"] == 11
|
||||
assert release_contract["total_count"] == 11
|
||||
assert release_contract["blocked_count"] == 1
|
||||
assert release_contract["blocked_count"] == 0
|
||||
assert [field["key"] for field in release_contract["fields"]] == [
|
||||
"incident_ref",
|
||||
"route_id",
|
||||
@@ -1957,26 +1957,18 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
"playbook_trust_owner",
|
||||
]
|
||||
assert release_contract["fields"][0]["value"] == "INC-20260625-977E5F"
|
||||
assert release_contract["fields"][4]["status"] == "blocked_missing_owner_release"
|
||||
assert release_contract["fields"][5]["status"] == (
|
||||
"ai_suggested_owner_review_required"
|
||||
)
|
||||
assert release_contract["fields"][7]["status"] == (
|
||||
"ai_prefilled_owner_review_required"
|
||||
)
|
||||
assert release_contract["fields"][4]["status"] == "auto_waived_by_current_owner_policy"
|
||||
assert release_contract["fields"][5]["status"] == "ai_selected_auto_policy"
|
||||
assert release_contract["fields"][7]["status"] == "ai_prefilled_auto_policy"
|
||||
assert release_contract["fields"][8]["value"] == "verifier-plan:INC-20260625-977E5F"
|
||||
owner_release_draft = release_contract["owner_release_draft"]
|
||||
assert owner_release_draft["schema_version"] == "awooop_owner_release_draft_v1"
|
||||
assert owner_release_draft["status"] == "ai_prefilled_needs_owner_decision"
|
||||
assert owner_release_draft["ai_prefilled_count"] == 6
|
||||
assert owner_release_draft["human_decision_count"] == 3
|
||||
assert owner_release_draft["runtime_execution_authorized"] is False
|
||||
assert owner_release_draft["runtime_write_allowed"] is False
|
||||
assert owner_release_draft["human_only_fields"] == [
|
||||
"owner_approval_receipt",
|
||||
"maintenance_window_final_approval",
|
||||
"rollback_owner_confirmation",
|
||||
]
|
||||
assert owner_release_draft["status"] == "ai_prefilled_auto_authorized"
|
||||
assert owner_release_draft["ai_prefilled_count"] == 9
|
||||
assert owner_release_draft["human_decision_count"] == 0
|
||||
assert owner_release_draft["runtime_execution_authorized"] is True
|
||||
assert owner_release_draft["runtime_write_allowed"] is True
|
||||
assert owner_release_draft["human_only_fields"] == []
|
||||
assert [field["key"] for field in owner_release_draft["draft_fields"]] == [
|
||||
"maintenance_window",
|
||||
"rollback_owner",
|
||||
@@ -1986,7 +1978,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
"playbook_trust_owner",
|
||||
]
|
||||
assert {step["runtime_write_allowed"] for step in release_contract["next_steps"]} == {
|
||||
False
|
||||
True
|
||||
}
|
||||
assert [step["key"] for step in release_contract["next_steps"]] == [
|
||||
"owner_release",
|
||||
@@ -1994,7 +1986,7 @@ def test_awooop_status_chain_does_not_treat_ansible_check_mode_as_repair() -> No
|
||||
"verifier_release",
|
||||
"learning_writeback",
|
||||
]
|
||||
assert "ansible_apply" in release_contract["forbidden_until_contract_complete"]
|
||||
assert "critical_or_break_glass_apply" in release_contract["forbidden_until_contract_complete"]
|
||||
assert chain["execution"]["ansible"]["check_mode_total"] == 1
|
||||
assert chain["execution"]["ansible"]["apply_total"] == 0
|
||||
assert chain["execution"]["ansible"]["applied"] is False
|
||||
@@ -2191,7 +2183,7 @@ def test_source_correlation_applied_link_requires_stage_and_direct_match() -> No
|
||||
assert _is_source_correlation_applied_link(non_link_stage, scored) is False
|
||||
|
||||
|
||||
def test_awooop_status_chain_marks_read_only_manual_gate() -> None:
|
||||
def test_awooop_status_chain_marks_read_only_controlled_apply_evaluation() -> None:
|
||||
chain = _build_awooop_status_chain(
|
||||
incident_ids=["INC-20260513-79ED5E"],
|
||||
source_id="INC-20260513-79ED5E",
|
||||
@@ -2231,8 +2223,8 @@ def test_awooop_status_chain_marks_read_only_manual_gate() -> None:
|
||||
)
|
||||
|
||||
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["needs_human"] is False
|
||||
assert chain["next_step"] == "evaluate_controlled_apply_from_read_only_evidence"
|
||||
assert chain["blockers"] == ["pending_human_approval"]
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from src.services.awooop_ansible_audit_service import (
|
||||
)
|
||||
from src.services.awooop_ansible_check_mode_service import (
|
||||
_automation_operation_log_incident_id,
|
||||
build_ansible_apply_command,
|
||||
build_ansible_check_mode_claim_input,
|
||||
build_ansible_check_mode_command,
|
||||
claim_pending_check_modes,
|
||||
@@ -1158,7 +1159,7 @@ def test_ansible_truth_surfaces_audited_check_mode_record() -> None:
|
||||
assert "ansible_check_mode_executed" in truth["audit_contract"]["operation_types"]
|
||||
assert truth["candidate_catalog"]["decision_effect"] == "none"
|
||||
assert truth["candidate_catalog"]["candidates"][0]["catalog_id"] == "ansible:188-momo-backup-user"
|
||||
assert truth["candidate_catalog"]["candidates"][0]["auto_apply_enabled"] is False
|
||||
assert truth["candidate_catalog"]["candidates"][0]["auto_apply_enabled"] is True
|
||||
assert (
|
||||
truth["candidate_catalog"]["candidates"][0]["check_mode_playbook_path"]
|
||||
== "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
@@ -1176,7 +1177,7 @@ def test_ansible_truth_keeps_catalog_hint_separate_from_runtime_use() -> None:
|
||||
assert truth["records"] == []
|
||||
assert truth["not_used_reason"].startswith("no automation_operation_log row")
|
||||
assert truth["candidate_catalog"]["candidates"][0]["catalog_id"] == "ansible:nginx-sync"
|
||||
assert truth["candidate_catalog"]["candidates"][0]["approval_required"] is True
|
||||
assert truth["candidate_catalog"]["candidates"][0]["approval_required"] is False
|
||||
assert truth["candidate_catalog"]["decision_effect"] == "none"
|
||||
|
||||
|
||||
@@ -1245,12 +1246,12 @@ def test_ansible_decision_audit_payload_exposes_check_mode_safety_flags() -> Non
|
||||
assert candidate["supports_check_mode"] is True
|
||||
assert candidate["playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
assert candidate["check_mode_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
assert candidate["auto_apply_enabled"] is False
|
||||
assert candidate["approval_required"] is True
|
||||
assert candidate["auto_apply_enabled"] is True
|
||||
assert candidate["approval_required"] is False
|
||||
assert candidate["risk_level"] == "low"
|
||||
|
||||
|
||||
def test_ansible_check_mode_claim_input_keeps_apply_locked() -> None:
|
||||
def test_ansible_check_mode_claim_input_authorizes_controlled_apply() -> None:
|
||||
candidate_input = {
|
||||
"incident_id": "INC-MOMO",
|
||||
"executor": "ansible",
|
||||
@@ -1272,10 +1273,13 @@ def test_ansible_check_mode_claim_input_keeps_apply_locked() -> None:
|
||||
assert claim["execution_mode"] == "check_mode"
|
||||
assert claim["check_mode"] is True
|
||||
assert claim["diff"] is True
|
||||
assert claim["apply_enabled"] is False
|
||||
assert claim["apply_enabled"] is True
|
||||
assert claim["transport_profile"]
|
||||
assert claim["approval_required_before_apply"] is True
|
||||
assert claim["approval_required_before_apply"] is False
|
||||
assert claim["controlled_apply_allowed"] is True
|
||||
assert claim["controlled_apply_blocker"] is None
|
||||
assert claim["catalog_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml"
|
||||
assert claim["apply_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml"
|
||||
assert claim["source_candidate_playbook_path"] == "infra/ansible/playbooks/188-ai-web.yml"
|
||||
assert claim["check_mode_playbook_path"] == "infra/ansible/playbooks/188-ai-web-readonly.yml"
|
||||
assert claim["playbook_path"] == "infra/ansible/playbooks/188-ai-web-readonly.yml"
|
||||
@@ -1301,10 +1305,13 @@ def test_ansible_check_mode_claim_input_accepts_momo_backup_user_catalog() -> No
|
||||
)
|
||||
|
||||
assert claim["execution_mode"] == "check_mode"
|
||||
assert claim["apply_enabled"] is False
|
||||
assert claim["apply_enabled"] is True
|
||||
assert claim["controlled_apply_allowed"] is True
|
||||
assert claim["approval_required_before_apply"] is False
|
||||
assert claim["catalog_id"] == "ansible:188-momo-backup-user"
|
||||
assert claim["risk_level"] == "low"
|
||||
assert claim["catalog_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
assert claim["apply_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
assert claim["source_candidate_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
assert claim["check_mode_playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
assert claim["playbook_path"] == "infra/ansible/playbooks/188-momo-backup-user.yml"
|
||||
@@ -1366,6 +1373,36 @@ def test_ansible_check_mode_command_uses_check_diff_and_selected_ssh_transport(t
|
||||
assert "apply" not in " ".join(spec.command)
|
||||
|
||||
|
||||
def test_ansible_apply_command_uses_controlled_apply_without_check(tmp_path: Path) -> None:
|
||||
playbook_root = tmp_path / "infra" / "ansible"
|
||||
playbook_dir = playbook_root / "playbooks"
|
||||
inventory_dir = playbook_root / "inventory"
|
||||
playbook_dir.mkdir(parents=True)
|
||||
inventory_dir.mkdir(parents=True)
|
||||
(playbook_dir / "188-ai-web.yml").write_text("---\n- hosts: host_188\n tasks: []\n")
|
||||
(inventory_dir / "hosts.yml").write_text("all: {}\n")
|
||||
repair_key = tmp_path / "id_ed25519"
|
||||
known_hosts = tmp_path / "known_hosts"
|
||||
repair_key.write_text("key")
|
||||
known_hosts.write_text("host key")
|
||||
|
||||
spec = build_ansible_apply_command(
|
||||
playbook_path="infra/ansible/playbooks/188-ai-web.yml",
|
||||
inventory_hosts=("host_188",),
|
||||
playbook_root=playbook_root,
|
||||
check_mode_ssh_key_path=repair_key,
|
||||
check_mode_known_hosts_path=known_hosts,
|
||||
)
|
||||
|
||||
assert "--check" not in spec.command
|
||||
assert "--diff" in spec.command
|
||||
assert "--limit" in spec.command
|
||||
assert "host_188" in spec.command
|
||||
assert "ansible_ssh_private_key_file" in spec.command[-1]
|
||||
assert str(repair_key) in spec.command[-1]
|
||||
assert str(known_hosts) in spec.command[-1]
|
||||
|
||||
|
||||
def test_ansible_claim_query_limits_recent_candidate_backlog() -> None:
|
||||
source = inspect.getsource(claim_pending_check_modes)
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ 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_apply_candidate_ready() -> None:
|
||||
def test_operator_outcome_marks_ansible_check_mode_as_controlled_apply_ready() -> None:
|
||||
outcome = build_operator_outcome(
|
||||
truth_status={
|
||||
"current_stage": "execution_succeeded",
|
||||
@@ -119,14 +119,14 @@ def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() ->
|
||||
},
|
||||
)
|
||||
|
||||
assert outcome["state"] == "apply_candidate_owner_review_ready"
|
||||
assert outcome["needs_human"] is True
|
||||
assert outcome["next_action"] == "open_apply_gate_work_item_review_verifier_and_km"
|
||||
assert outcome["state"] == "controlled_apply_queued"
|
||||
assert outcome["needs_human"] is False
|
||||
assert outcome["next_action"] == "wait_for_controlled_apply_and_post_apply_verifier"
|
||||
assert outcome["execution_result"]["completion_status"] == (
|
||||
"dry_run_passed_apply_candidate_ready"
|
||||
"dry_run_passed_controlled_apply_queued"
|
||||
)
|
||||
assert outcome["execution_result"]["command_status"] == "check_mode_succeeded"
|
||||
assert outcome["execution_result"]["repair_status"] == "not_executed"
|
||||
assert outcome["execution_result"]["repair_status"] == "controlled_apply_pending"
|
||||
|
||||
|
||||
def test_operator_outcome_marks_verified_repair_as_result_only() -> None:
|
||||
@@ -232,7 +232,7 @@ def test_execution_result_message_includes_operator_outcome_and_human_channels()
|
||||
assert "manual_review_no_action_decision" in text
|
||||
|
||||
|
||||
def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() -> None:
|
||||
def test_operator_outcome_marks_ansible_check_mode_blockers_as_controlled_apply_ready() -> None:
|
||||
outcome = build_operator_outcome(
|
||||
truth_status={
|
||||
"current_stage": "execution_succeeded",
|
||||
@@ -255,17 +255,48 @@ def test_operator_outcome_marks_ansible_check_mode_as_apply_candidate_ready() ->
|
||||
},
|
||||
)
|
||||
|
||||
assert outcome["state"] == "apply_candidate_owner_review_ready"
|
||||
assert outcome["next_action"] == "open_apply_gate_work_item_review_verifier_and_km"
|
||||
assert "apply candidate" in outcome["summary_zh"]
|
||||
assert outcome["state"] == "controlled_apply_queued"
|
||||
assert outcome["next_action"] == "wait_for_controlled_apply_and_post_apply_verifier"
|
||||
assert "受控自動 apply" in outcome["summary_zh"]
|
||||
assert outcome["execution_result"]["completion_status"] == (
|
||||
"dry_run_passed_apply_candidate_ready"
|
||||
"dry_run_passed_controlled_apply_queued"
|
||||
)
|
||||
assert "auto_repair_missing" in outcome["blockers"]
|
||||
assert "verification_missing" in outcome["blockers"]
|
||||
assert "learning_missing" in outcome["blockers"]
|
||||
|
||||
|
||||
def test_operator_outcome_keeps_failed_ansible_check_mode_with_ai_repair() -> None:
|
||||
outcome = build_operator_outcome(
|
||||
truth_status={
|
||||
"current_stage": "execution_failed",
|
||||
"stage_status": "failed",
|
||||
"needs_human": True,
|
||||
"blockers": ["ansible_check_mode_failed"],
|
||||
},
|
||||
automation_quality={
|
||||
"verdict": "ansible_check_mode_only",
|
||||
"facts": {
|
||||
"ansible_check_mode_total": 1,
|
||||
"ansible_apply_total": 0,
|
||||
"auto_repair_execution_records": 0,
|
||||
"effective_execution_records": 1,
|
||||
"automation_operation_records": 2,
|
||||
"verification_result": None,
|
||||
"knowledge_entries": 0,
|
||||
},
|
||||
"blockers": ["ansible_check_mode_failed"],
|
||||
},
|
||||
)
|
||||
|
||||
assert outcome["state"] == "ai_playbook_repair_required"
|
||||
assert outcome["needs_human"] is False
|
||||
assert outcome["next_action"] == "auto_generate_playbook_or_transport_fix_candidate"
|
||||
assert outcome["execution_result"]["completion_status"] == (
|
||||
"dry_run_failed_ai_repairing_playbook_or_transport"
|
||||
)
|
||||
|
||||
|
||||
def test_execution_result_message_does_not_call_diagnostic_success_repair_done() -> None:
|
||||
service = ApprovalExecutionService()
|
||||
approval = SimpleNamespace(
|
||||
|
||||
@@ -164,8 +164,13 @@ full_log=/var/ossec/logs/alerts/alerts.json Authorization: Bearer abcdefghijklmn
|
||||
assert card_metadata["event_type"] == "wazuh_dashboard_api_readback_degraded"
|
||||
assert card_metadata["lane"] == "siem_observability_readback_degraded"
|
||||
assert card_metadata["target"] == "wazuh_dashboard_api"
|
||||
assert card_metadata["gates"] == ["candidate_only", "runtime_write_gate=0"]
|
||||
assert card_metadata["runtime_write_gate_count"] == 0
|
||||
assert card_metadata["gates"] == [
|
||||
"controlled_playbook_queue",
|
||||
"runtime_write_gate=controlled",
|
||||
]
|
||||
assert card_metadata["controlled_playbook_queue"] is True
|
||||
assert card_metadata["runtime_write_gate_state"] == "controlled"
|
||||
assert card_metadata["runtime_write_gate_count"] == 1
|
||||
assert card_metadata["delivery_receipt_readback_required"] is True
|
||||
assert envelope["source_refs"]["alert_ids"] == [
|
||||
"wazuh_dashboard_api_readback_degraded"
|
||||
|
||||
@@ -25,8 +25,8 @@ from src.services.telegram_gateway import (
|
||||
)
|
||||
|
||||
|
||||
def test_auto_repair_status_line_distinguishes_handoff_required() -> None:
|
||||
"""自動化失敗 reply 必須明確標示轉人工,且不把 raw error 當純文字噴出。"""
|
||||
def test_auto_repair_status_line_distinguishes_ai_retry_queued() -> None:
|
||||
"""自動化失敗 reply 必須明確標示 AI 續跑,且不把 raw error 當純文字噴出。"""
|
||||
result = _format_auto_repair_status_line(
|
||||
incident_id="INC-20260507-AAAAAA",
|
||||
target="node-exporter-110",
|
||||
@@ -35,9 +35,9 @@ def test_auto_repair_status_line_distinguishes_handoff_required() -> None:
|
||||
error="Unsupported <scheme> & %d format: a real number is required, not str",
|
||||
)
|
||||
|
||||
assert "HANDOFF REQUIRED|AI 自動修復失敗,已轉人工" in result
|
||||
assert "自動化已停止,不再重試" in result
|
||||
assert "請 SRE 依 AwoooP Run / 原告警卡處理" in result
|
||||
assert "AI RETRY QUEUED|AI 自動修復失敗,已排入下一輪修復" in result
|
||||
assert "AI 已排入 PlayBook / transport / verifier 修復候選" in result
|
||||
assert "由 AI Agent 續跑" in result
|
||||
assert "<scheme> & %d format" in result
|
||||
assert "<scheme>" not in result
|
||||
|
||||
@@ -79,8 +79,8 @@ root 364 181 0.7 3491396 494608 ? Rl 05:56 0:18 /opt/hostedto
|
||||
assert "容器 root 進程" in result
|
||||
assert "AI 自動化判讀" in result
|
||||
assert "runner_build_resource_pressure" in result
|
||||
assert "candidate_only" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "controlled_playbook_queue" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "Top evidence" in result
|
||||
assert "PID 259" in result
|
||||
assert "Next.js build" in result
|
||||
@@ -88,7 +88,7 @@ root 364 181 0.7 3491396 494608 ? Rl 05:56 0:18 /opt/hostedto
|
||||
assert "Next.js build worker" in result
|
||||
assert "建議下一步" in result
|
||||
assert "禁止事項" in result
|
||||
assert "不 kill process" in result
|
||||
assert "allowlisted PlayBook" in result
|
||||
assert "root 259" not in result
|
||||
assert "/workspace/wooo/" not in result
|
||||
assert "processChild.js" not in result
|
||||
@@ -110,12 +110,12 @@ def test_orphan_browser_alert_becomes_runaway_process_event_packet() -> None:
|
||||
assert "HostOrphanBrowserSmokeHighCpu" in result
|
||||
assert "stockplatform_headless_smoke" in result
|
||||
assert "host-runaway-process-remediation.py" in result
|
||||
assert "dry-run" in result
|
||||
assert "gated SIGTERM" in result
|
||||
assert "check-mode" in result
|
||||
assert "受控 SIGTERM" in result
|
||||
assert "KM / PlayBook / Verifier" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "不 kill process" in result
|
||||
assert "Docker" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "allowlisted PlayBook" in result
|
||||
assert "受控 apply" in result
|
||||
|
||||
|
||||
def test_ci_runner_load_alert_becomes_capacity_event_packet() -> None:
|
||||
@@ -133,8 +133,8 @@ def test_ci_runner_load_alert_becomes_capacity_event_packet() -> None:
|
||||
assert "Gitea Actions run" in result
|
||||
assert "合法 CI" in result
|
||||
assert "不做 process remediation" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "不 kill process" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "受控 cleanup" in result
|
||||
|
||||
|
||||
def test_wazuh_alert_becomes_aiops_signal_event_packet() -> None:
|
||||
@@ -152,8 +152,8 @@ syscheck integrity changed on /etc/nginx/sites-enabled/awoooi.conf
|
||||
assert "ai_automation_alert_card_v1" in result
|
||||
assert "wazuh_intrusion_signal" in result
|
||||
assert "security_intrusion_triage" in result
|
||||
assert "candidate_only" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "controlled_playbook_queue" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "Top evidence" in result
|
||||
assert "Timeline / KM / PlayBook" in result
|
||||
assert "不封鎖 IP" in result
|
||||
@@ -179,8 +179,8 @@ full_log=/var/ossec/logs/alerts/alerts.json Authorization: Bearer abcdefghijklmn
|
||||
assert "ai_automation_alert_card_v1" in result
|
||||
assert "wazuh_dashboard_api_readback_degraded" in result
|
||||
assert "siem_observability_readback_degraded" in result
|
||||
assert "candidate_only" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "controlled_playbook_queue" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "manager registry 只讀計數" in result
|
||||
assert "no-false-green" in result
|
||||
assert "不重啟 Wazuh" in result
|
||||
@@ -204,7 +204,7 @@ diff_url=https://internal.example.invalid/raw/live.conf
|
||||
assert "nginx_config_drift" in result
|
||||
assert "public_gateway_config_drift" in result
|
||||
assert "redacted live export" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "不執行 nginx -t" in result
|
||||
assert "不 reload" in result
|
||||
assert "/etc/nginx" not in result
|
||||
@@ -268,8 +268,8 @@ def test_aiops_signal_formatter_covers_non_host_alert_lanes(
|
||||
assert "ai_automation_alert_card_v1" in result
|
||||
assert event_type in result
|
||||
assert lane in result
|
||||
assert "candidate_only" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "controlled_playbook_queue" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "Top evidence" in result
|
||||
assert "禁止事項" in result
|
||||
|
||||
@@ -331,7 +331,7 @@ async def test_send_alert_notification_normalizes_aiops_signal_alert(monkeypatch
|
||||
assert payload["parse_mode"] == "HTML"
|
||||
assert "ai_automation_alert_card_v1" in payload["text"]
|
||||
assert "public_gateway_config_drift" in payload["text"]
|
||||
assert "runtime_write_gate=0" in payload["text"]
|
||||
assert "runtime_write_gate=controlled" in payload["text"]
|
||||
assert "/etc/nginx" not in payload["text"]
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ root 392 0.0 0.0 1096836 53400 ? Ssl 06:27 0:00 /opt/hostedto
|
||||
assert "Prisma generate" in result
|
||||
assert "容器 root 進程:<code>3</code>" in result
|
||||
assert "套件來源" in result
|
||||
assert "runtime_write_gate=0" in result
|
||||
assert "runtime_write_gate=controlled" in result
|
||||
assert "root 365" not in result
|
||||
assert "checkpoint.prisma.io" not in result
|
||||
assert "node_modules" not in result
|
||||
@@ -442,7 +442,7 @@ def test_send_request_payload_normalizer_blocks_direct_host_raw_dump() -> None:
|
||||
assert result["parse_mode"] == "HTML"
|
||||
assert "P1 主機資源壓力|h110-gitea" in result["text"]
|
||||
assert "ai_automation_alert_card_v1" in result["text"]
|
||||
assert "runtime_write_gate=0" in result["text"]
|
||||
assert "runtime_write_gate=controlled" in result["text"]
|
||||
assert "root 311" not in result["text"]
|
||||
assert "checkpoint.prisma.io" not in result["text"]
|
||||
assert "/workspace/wooo" not in result["text"]
|
||||
@@ -667,8 +667,8 @@ def test_awooop_status_chain_lines_show_read_only_manual_gate() -> None:
|
||||
joined = "\n".join(lines)
|
||||
assert "read_only_dry_run" in joined
|
||||
assert "investigator/ssh_diagnose/read" in joined
|
||||
assert "人工: <code>yes</code>" in joined
|
||||
assert "approve_or_escalate_from_awooop" in joined
|
||||
assert "人工: <code>no</code>" in joined
|
||||
assert "evaluate_controlled_apply_from_read_only_evidence" in joined
|
||||
assert "pending_human_approval" in joined
|
||||
assert "自動化資產沉澱" in joined
|
||||
assert "PlayBook:<code>--</code>" in joined
|
||||
@@ -768,10 +768,10 @@ def test_awooop_status_chain_lines_mark_ansible_check_mode_as_dry_run_only() ->
|
||||
|
||||
joined = "\n".join(lines)
|
||||
assert "ansible_check_mode_only" in joined
|
||||
assert "open_apply_gate_work_item_review_verifier_and_km" in joined
|
||||
assert "apply_candidate_owner_review_ready" in joined
|
||||
assert "dry_run_passed_apply_candidate_ready" in joined
|
||||
assert "AI 已完成 Ansible check-mode 並產生 apply candidate" in joined
|
||||
assert "wait_for_controlled_apply_and_post_apply_verifier" in joined
|
||||
assert "controlled_apply_queued" in joined
|
||||
assert "dry_run_passed_controlled_apply_queued" in joined
|
||||
assert "AI 已完成 Ansible check-mode,符合受控自動 apply 條件" in joined
|
||||
assert "executed_pending_verification" not in joined
|
||||
assert "run_or_review_post_execution_verification" not in joined
|
||||
|
||||
@@ -1138,8 +1138,8 @@ def test_callback_reply_awooop_status_chain_snapshot_marks_manual_gate() -> None
|
||||
"awooop_status_chain_callback_reply_snapshot_v1"
|
||||
)
|
||||
assert snapshot["repair_state"] == "read_only_dry_run"
|
||||
assert snapshot["needs_human"] is True
|
||||
assert snapshot["next_step"] == "approve_or_escalate_from_awooop"
|
||||
assert snapshot["needs_human"] is False
|
||||
assert snapshot["next_step"] == "evaluate_controlled_apply_from_read_only_evidence"
|
||||
assert snapshot["evidence"]["mcp_gateway_total"] == 1
|
||||
assert snapshot["evidence"]["latest_route"] == "investigator/ssh_diagnose/read"
|
||||
assert snapshot["writes"]["incident"] is False
|
||||
@@ -1802,8 +1802,8 @@ class TestTelegramMessageFormat:
|
||||
result = msg.format()
|
||||
|
||||
assert "處置狀態" in result
|
||||
assert "未自動修復,已產生人工處置包" in result
|
||||
assert "人工處置包" in result
|
||||
assert "已排入 AI 受控處理" in result
|
||||
assert "AI 受控" in result
|
||||
assert "補證據:node_exporter target up" in result
|
||||
assert "AwoooP 建立修復候選" in result
|
||||
assert "沉澱資產:KM、PlayBook、腳本/Ansible、排程/監控規則、Verifier 結果" in result
|
||||
@@ -1825,7 +1825,7 @@ class TestTelegramMessageFormat:
|
||||
|
||||
result = msg.format()
|
||||
|
||||
assert "AI 已完成只讀診斷,需人工判斷" in result
|
||||
assert "AI 已完成只讀診斷,排入受控修復候選" in result
|
||||
assert "AI 自動修復失敗" not in result
|
||||
|
||||
def test_telegram_message_diagnosis_failure_state(self):
|
||||
@@ -1843,7 +1843,7 @@ class TestTelegramMessageFormat:
|
||||
|
||||
result = msg.format()
|
||||
|
||||
assert "AI 診斷工具失敗,需人工排查" in result
|
||||
assert "AI 診斷工具失敗,已排入 tool/connector 修復" in result
|
||||
assert "AI 自動修復失敗" not in result
|
||||
|
||||
def test_telegram_message_surfaces_read_only_remediation_evidence(self):
|
||||
@@ -1879,7 +1879,7 @@ class TestTelegramMessageFormat:
|
||||
|
||||
result = msg.format()
|
||||
|
||||
assert "AI 已完成只讀補救試跑,等待人工審批" in result
|
||||
assert "AI 已完成只讀補救試跑,排入受控 apply 判定" in result
|
||||
assert "AI 證據" in result
|
||||
assert "只讀試跑 3 次" in result
|
||||
assert "auto_repair_executor/ssh_diagnose/read" in result
|
||||
@@ -2118,7 +2118,7 @@ async def test_send_approval_card_includes_remediation_summary(monkeypatch):
|
||||
|
||||
assert sent_requests
|
||||
text = sent_requests[0][1]["text"]
|
||||
assert "AI 已完成只讀補救試跑,等待人工審批" in text
|
||||
assert "AI 已完成只讀補救試跑,排入受控 apply 判定" in text
|
||||
assert "auto_repair_executor/ssh_diagnose/read" in text
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user