fix(ai): improve docker repair verification signals
Some checks failed
CD Pipeline / tests (push) Successful in 1m22s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 4m10s
CD Pipeline / post-deploy-checks (push) Has been cancelled

This commit is contained in:
Your Name
2026-06-01 19:27:36 +08:00
parent 2ce53829fc
commit 7f3722c7f7
8 changed files with 228 additions and 6 deletions

View File

@@ -274,3 +274,45 @@ def test_verification_coverage_payload_skips_when_no_auto_repair():
assert payload["reason"] == "no_auto_repair_executions_24h"
assert payload["evaluable"] is False
assert payload["remediation_queue"]["total"] == 0
def test_verification_coverage_payload_routes_observe_only_playbook_to_ticket():
payload = _build_verification_coverage_payload(
{
"total_auto": 1,
"successful_auto": 1,
"verified_auto": 1,
"verified_success": 0,
"verified_non_success": 1,
"unverified_auto": 0,
},
[],
[
{
"auto_repair_id": "are-3",
"incident_id": "INC-3",
"incident_status": "INVESTIGATING",
"incident_severity": "P2",
"alert_category": "infrastructure",
"alertname": "DockerContainerMemoryLimitPressure",
"auto_success": True,
"playbook_id": "PB-3",
"playbook_name": "Docker pressure diagnostic playbook",
"triggered_by": "auto_repair",
"risk_level": "LOW",
"verification_result": "degraded",
"auto_error": "",
"post_state_text": '{"ssh_diagnose": {"command": "docker stats --no-stream api"}}',
"evidence_summary": "SUCCESS: mcp:ssh_diagnose",
"auto_created_at": None,
"verification_collected_at": None,
},
],
)
item = payload["recent_non_success"][0]
assert item["failure_class"] == "observe_only_playbook"
assert item["next_step"] == "author_mutating_repair_step"
assert item["remediation_status"] == "needs_playbook_ticket"
assert item["remediation_action"] == "promote_diagnostic_to_repair_playbook"
assert payload["remediation_queue"]["needs_human"] == 1

View File

@@ -225,10 +225,13 @@ async def test_auto_repair_success_triggers_verify_and_learn(monkeypatch):
assert len(stub_verifier.calls) == 1, "verifier.verify() 應被呼叫一次"
assert stub_verifier.calls[0]["incident_id"] == incident.incident_id
assert stub_verifier.calls[0]["snapshot"] is None
assert stub_verifier.calls[0]["action_taken"].startswith(f"auto_repair:{playbook.playbook_id}")
assert "steps=Step 1:" in stub_verifier.calls[0]["action_taken"]
assert len(stub_learning.verification_calls) == 1, "record_verification_result 應被呼叫一次"
call = stub_learning.verification_calls[0]
assert call["incident_id"] == incident.incident_id
assert call["action_taken"] == stub_verifier.calls[0]["action_taken"]
assert call["verification_result"] == "success"
assert call["matched_playbook_id"] == playbook.playbook_id

View File

@@ -387,6 +387,34 @@ class TestSuggestTools:
assert "prometheus_query" in names
assert "kubectl_describe" not in names
@pytest.mark.asyncio
async def test_docker_container_alert_does_not_treat_container_as_pod_locator(self):
registry = MCPToolRegistry()
ssh_provider = _StubProvider("ssh_host", ["ssh_diagnose", "ssh_get_container_status"])
k8s_provider = _StubProvider("kubernetes", ["kubectl_describe", "k8s_get_pod_logs"])
prometheus_provider = _StubProvider("prometheus", ["prometheus_query"])
await registry.register_provider(k8s_provider)
await registry.register_provider(ssh_provider)
await registry.register_provider(prometheus_provider)
tools = registry.suggest_tools(
alertname="DockerContainerMemoryLimitPressure",
incident_labels={
"namespace": "default",
"container": "momo-pro-system",
"container_name": "momo-pro-system",
"host": "188",
},
max_tools=8,
)
names = [reg.tool.name for reg in tools]
assert "ssh_diagnose" in names
assert "ssh_get_container_status" in names
assert "prometheus_query" in names
assert "kubectl_describe" not in names
assert "k8s_get_pod_logs" not in names
def test_get_all_tools_returns_all(self):
registry = MCPToolRegistry()
provider = _StubProvider("test", [])

View File

@@ -136,6 +136,23 @@ class TestAssessRecovery:
action = "auto_repair_playbook:PB-TEST mcp:ssh_diagnose docker stats"
assert _assess_recovery(pre, post, action) == "degraded"
def test_diagnosis_only_without_pre_state_is_not_verified_repair(self):
"""只有 read-only SSH 診斷且無 pre-state 時,也不能被誤算成真修復。"""
post = {"ssh_diagnose": {"command": "docker stats --no-stream api", "stdout": '"Status": "running"'}}
action = "auto_repair:PB-TEST steps=Step 1: docker stats -> SUCCESS: mcp:ssh_diagnose"
assert _assess_recovery(None, post, action) == "degraded"
def test_docker_running_after_restart_is_success(self):
"""Docker restart 後看到 docker ps/inspect running可驗證為 success。"""
post = {
"ssh_get_container_status": {
"command": "docker ps -a --filter name=api",
"stdout": "api Up 12 seconds",
}
}
action = "auto_repair:PB-TEST steps=Step 1: docker restart api -> SUCCESS"
assert _assess_recovery(None, post, action) == "success"
def test_pre_running_post_running_delete_is_success(self):
"""kubectl delete 動作,前後都 Running → success"""
pre = {"status": "Running"}
@@ -417,6 +434,25 @@ class _PrometheusRegistry:
]
class _DockerRegistry:
def __init__(self, provider: _CaptureProvider) -> None:
self.provider = provider
def suggest_tools(self, alertname: str = "", incident_labels: dict | None = None) -> list[RegisteredTool]:
return [
RegisteredTool(
tool=MCPTool(
name="ssh_get_container_status",
description="",
input_schema={},
server_name="capture",
),
provider=self.provider,
dimensions=[SensorDimension.D3_METRICS],
)
]
class _DbContext:
async def __aenter__(self) -> object:
return object()
@@ -501,6 +537,23 @@ class TestCollectPostStateAuditContext:
assert 'host="110"' in query
assert 'container_name="momo-scheduler"' in query
@pytest.mark.asyncio
async def test_collect_post_state_sends_docker_container_target(self):
provider = _CaptureProvider()
verifier = PostExecutionVerifier()
verifier._registry = _DockerRegistry(provider)
incident = _stub_incident(alertname="DockerContainerUnhealthy")
incident.signals[0].labels.update({
"host": "188",
"container_name": "momo-pro-system",
})
await verifier._collect_post_state(incident)
assert provider.seen_parameters is not None
assert provider.seen_parameters["container_name"] == "momo-pro-system"
assert provider.seen_parameters["filter_name"] == "momo-pro-system"
class TestPrometheusQueryBuilder:
def test_docker_memory_alert_query_is_not_empty(self):