All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m38s
CD Pipeline / build-and-deploy (push) Successful in 6m58s
CD Pipeline / post-deploy-checks (push) Successful in 1m42s
237 lines
7.1 KiB
Python
237 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
from contextlib import asynccontextmanager
|
|
from datetime import UTC, datetime
|
|
from types import SimpleNamespace
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
from src.api.v1.webhooks import _controlled_ai_policy_allows
|
|
from src.core.trust_engine import classify_risk, get_required_signatures
|
|
from src.models.approval import ApprovalStatus, RiskLevel
|
|
from src.services import approval_db as approval_db_module
|
|
from src.services.approval_db import ApprovalDBService, approval_request_to_record_data
|
|
from src.services.telegram_gateway import TelegramGateway, TelegramMessage
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"risk_level",
|
|
[RiskLevel.LOW, RiskLevel.MEDIUM, RiskLevel.HIGH],
|
|
)
|
|
def test_noncritical_risks_are_auto_authorized_for_controlled_apply(
|
|
risk_level: RiskLevel,
|
|
) -> None:
|
|
assert get_required_signatures(risk_level) == 0
|
|
assert _controlled_ai_policy_allows(risk_level) is True
|
|
|
|
|
|
def test_critical_risk_remains_break_glass() -> None:
|
|
assert get_required_signatures(RiskLevel.CRITICAL) == 1
|
|
assert _controlled_ai_policy_allows(RiskLevel.CRITICAL) is False
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"action",
|
|
[
|
|
"host reboot 192.168.0.110",
|
|
"kubectl get secret awoooi-secrets",
|
|
"git force push main",
|
|
"restore database from snapshot",
|
|
],
|
|
)
|
|
def test_critical_action_overrides_explicit_high_risk(action: str) -> None:
|
|
assert classify_risk(action, explicit_level=RiskLevel.HIGH) == RiskLevel.CRITICAL
|
|
|
|
|
|
def test_medium_approval_record_is_created_auto_approved() -> None:
|
|
request = SimpleNamespace(
|
|
action="kubectl rollout restart deployment/api",
|
|
description="bounded rollout",
|
|
metadata={"source": "alertmanager"},
|
|
blast_radius=None,
|
|
dry_run_checks=[],
|
|
requested_by="OpenClaw",
|
|
expires_at=None,
|
|
incident_id="INC-CONTROLLED",
|
|
matched_playbook_id=None,
|
|
)
|
|
|
|
data = approval_request_to_record_data(
|
|
request,
|
|
RiskLevel.MEDIUM,
|
|
get_required_signatures(RiskLevel.MEDIUM),
|
|
)
|
|
|
|
assert data["status"] == ApprovalStatus.APPROVED
|
|
assert data["required_signatures"] == 0
|
|
assert data["resolved_at"] is not None
|
|
|
|
|
|
def test_medium_telegram_card_is_automation_progress_not_action_required() -> None:
|
|
message = TelegramMessage(
|
|
status_emoji="⚠️",
|
|
risk_level="MEDIUM",
|
|
resource_name="awoooi-api",
|
|
root_cause="restart required",
|
|
suggested_action="kubectl rollout restart deployment/awoooi-api",
|
|
estimated_downtime="30s",
|
|
approval_id="approval-1",
|
|
incident_id="INC-CONTROLLED",
|
|
controlled_auto_authorized=True,
|
|
)
|
|
|
|
rendered = message.format()
|
|
|
|
assert "AI CONTROLLED AUTOMATION" in rendered
|
|
assert "ACTION REQUIRED" not in rendered
|
|
assert "awaiting_approval" not in rendered
|
|
assert "check>apply>verify>writeback" in rendered
|
|
|
|
|
|
def test_critical_telegram_card_stays_break_glass() -> None:
|
|
message = TelegramMessage(
|
|
status_emoji="🚨",
|
|
risk_level="CRITICAL",
|
|
resource_name="database",
|
|
root_cause="destructive restore requested",
|
|
suggested_action="restore database from snapshot",
|
|
estimated_downtime="unknown",
|
|
approval_id="approval-critical",
|
|
incident_id="INC-CRITICAL",
|
|
)
|
|
|
|
rendered = message.format()
|
|
|
|
assert "BREAK-GLASS REQUIRED" in rendered
|
|
assert "AI CONTROLLED AUTOMATION" not in rendered
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_controlled_automation_keyboard_has_no_approval_buttons() -> None:
|
|
gateway = TelegramGateway()
|
|
|
|
keyboard = await gateway._build_inline_keyboard(
|
|
approval_id="approval-1",
|
|
incident_id="INC-CONTROLLED",
|
|
suggested_action="kubectl rollout restart deployment/awoooi-api",
|
|
controlled_auto_authorized=True,
|
|
)
|
|
button_texts = {
|
|
button["text"]
|
|
for row in keyboard["inline_keyboard"]
|
|
for button in row
|
|
}
|
|
|
|
assert "✅ 批准" not in button_texts
|
|
assert "❌ 拒絕" not in button_texts
|
|
assert "🧰 處置包" in button_texts
|
|
assert "📊 歷史" in button_texts
|
|
|
|
|
|
def _approval_record(*, action: str, risk_level: RiskLevel) -> SimpleNamespace:
|
|
now = datetime.now(UTC)
|
|
return SimpleNamespace(
|
|
id=str(uuid4()),
|
|
action=action,
|
|
description="legacy pending alert",
|
|
status=ApprovalStatus.PENDING,
|
|
risk_level=risk_level,
|
|
required_signatures=1,
|
|
current_signatures=0,
|
|
signatures=[],
|
|
blast_radius={
|
|
"affected_pods": 1,
|
|
"estimated_downtime": "30s",
|
|
"related_services": ["awoooi-api"],
|
|
"data_impact": "write",
|
|
},
|
|
dry_run_checks=[],
|
|
requested_by="OpenClaw",
|
|
created_at=now,
|
|
expires_at=None,
|
|
resolved_at=None,
|
|
rejection_reason=None,
|
|
extra_metadata={"source": "alertmanager"},
|
|
fingerprint="fingerprint",
|
|
hit_count=1,
|
|
last_seen_at=now,
|
|
incident_id="INC-LEGACY-PENDING",
|
|
matched_playbook_id=None,
|
|
telegram_message_id=None,
|
|
telegram_chat_id=None,
|
|
)
|
|
|
|
|
|
class _FakeResult:
|
|
def __init__(self, record: SimpleNamespace) -> None:
|
|
self._record = record
|
|
|
|
def scalar_one_or_none(self) -> SimpleNamespace:
|
|
return self._record
|
|
|
|
|
|
class _FakeDB:
|
|
def __init__(self, record: SimpleNamespace) -> None:
|
|
self.record = record
|
|
self.events: list[object] = []
|
|
|
|
async def execute(self, _statement: object) -> _FakeResult:
|
|
return _FakeResult(self.record)
|
|
|
|
def add(self, event: object) -> None:
|
|
self.events.append(event)
|
|
|
|
async def flush(self) -> None:
|
|
return None
|
|
|
|
async def refresh(self, _record: object) -> None:
|
|
return None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_legacy_medium_pending_gate_is_durably_promoted(monkeypatch) -> None:
|
|
record = _approval_record(
|
|
action="kubectl rollout restart deployment/awoooi-api",
|
|
risk_level=RiskLevel.MEDIUM,
|
|
)
|
|
db = _FakeDB(record)
|
|
|
|
@asynccontextmanager
|
|
async def fake_db_context():
|
|
yield db
|
|
|
|
monkeypatch.setattr(approval_db_module, "get_db_context", fake_db_context)
|
|
|
|
approval, promoted = await ApprovalDBService().apply_current_owner_policy(record.id)
|
|
|
|
assert promoted is True
|
|
assert approval is not None
|
|
assert approval.status == ApprovalStatus.APPROVED
|
|
assert approval.required_signatures == 0
|
|
assert approval.metadata["legacy_pending_promoted"] is True
|
|
assert len(db.events) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_legacy_critical_pending_gate_is_not_promoted(monkeypatch) -> None:
|
|
record = _approval_record(
|
|
action="host reboot 192.168.0.110",
|
|
risk_level=RiskLevel.HIGH,
|
|
)
|
|
db = _FakeDB(record)
|
|
|
|
@asynccontextmanager
|
|
async def fake_db_context():
|
|
yield db
|
|
|
|
monkeypatch.setattr(approval_db_module, "get_db_context", fake_db_context)
|
|
|
|
approval, promoted = await ApprovalDBService().apply_current_owner_policy(record.id)
|
|
|
|
assert promoted is False
|
|
assert approval is not None
|
|
assert approval.status == ApprovalStatus.PENDING
|
|
assert approval.required_signatures == 1
|
|
assert db.events == []
|