Files
awoooi/apps/api/tests/test_telegram_message_templates.py
Your Name ea5ad040da
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 1m16s
CD Pipeline / build-and-deploy (push) Successful in 3m39s
CD Pipeline / post-deploy-checks (push) Successful in 1m18s
fix(telegram): clarify automation notification state
2026-05-06 20:59:58 +08:00

397 lines
12 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
test_telegram_message_templates.py - Telegram 訊息模板測試
2026-03-29 ogt: 新增,驗證 6 種新訊息模板格式正確
符合 feedback_no_mock_testing.md 規範 - 測試實際格式化輸出
"""
import pytest
import src.services.telegram_gateway as telegram_gateway_module
from src.services.telegram_gateway import (
DailySummaryMessage,
DeploySuccessMessage,
RateLimitMessage,
RepairReportMessage,
ResourceWarnMessage,
SentryErrorMessage,
TelegramMessage,
TelegramGateway,
)
class TestTelegramMessageFormat:
"""測試現有 TelegramMessage 格式化"""
def test_telegram_message_format_basic(self):
"""測試基本訊息格式化"""
msg = TelegramMessage(
status_emoji="🚨",
risk_level="CRITICAL",
resource_name="test-pod-123",
root_cause="Test root cause",
suggested_action="Restart pod",
estimated_downtime="~30s",
approval_id="INC-20260329-0001",
)
result = msg.format()
assert "🚨" in result
assert "嚴重" in result
assert "test-pod-123" in result
assert "處置狀態" in result
assert "規則建議待審批" in result
assert "AI 自動化鏈路" in result
assert "OpenClaw" in result
assert "NemoTron" in result
assert "ElephantAlpha" in result
assert len(result) <= 4096 # Telegram HTML message limit
def test_telegram_message_ai_proposal_marks_approval_wait(self):
"""有 AI 信心分數的修復建議必須標示為 AI 待審批。"""
msg = TelegramMessage(
status_emoji="⚠️",
risk_level="MEDIUM",
resource_name="awoooi-api",
root_cause="CPU sustained high",
suggested_action="kubectl rollout restart deployment/awoooi-api",
estimated_downtime="~30s",
approval_id="INC-20260506-0000",
confidence=0.82,
ai_provider="ollama_gcp_a",
)
result = msg.format()
assert "處置狀態" in result
assert "AI 已提出修復建議,等待人工批准" in result
def test_telegram_message_no_action_marks_manual_judgement(self):
"""NO_ACTION 卡片必須一眼看得出需要人工判斷。"""
msg = TelegramMessage(
status_emoji="",
risk_level="LOW",
resource_name="node-exporter-110",
root_cause="規則命中但沒有安全可執行動作",
suggested_action="NO_ACTION",
estimated_downtime="unknown",
approval_id="INC-20260506-0001",
)
result = msg.format()
assert "處置狀態" in result
assert "AI 無可安全執行動作,需人工判斷" in result
def test_telegram_message_with_token_cost(self):
"""測試含 Token/Cost 的訊息"""
msg = TelegramMessage(
status_emoji="⚠️",
risk_level="MEDIUM",
resource_name="api-pod",
root_cause="High CPU",
suggested_action="Scale up",
estimated_downtime="0s",
approval_id="INC-20260329-0002",
ai_tokens=1500,
ai_cost=0.0015,
)
result = msg.format()
assert "💰 Tokens: 1,500 / $0.0015" in result
@pytest.mark.asyncio
async def test_append_incident_update_deduplicates_same_status(monkeypatch):
"""同一 Incident 的相同狀態更新 5 分鐘內不可重複洗版。"""
class FakeRedis:
def __init__(self):
self.set_calls = 0
async def get(self, key):
assert key == "tg_msg:INC-DEDUP"
return "12345"
async def set(self, *args, **kwargs):
self.set_calls += 1
assert kwargs["nx"] is True
assert kwargs["ex"] > 0
return self.set_calls == 1
fake_redis = FakeRedis()
sent_requests = []
gateway = TelegramGateway()
async def fake_send_request(method, payload):
sent_requests.append((method, payload))
return {"ok": True}
monkeypatch.setattr(telegram_gateway_module, "get_redis", lambda: fake_redis)
monkeypatch.setattr(gateway, "_send_request", fake_send_request)
status_line = "🤖❌ <b>[AUTO] AI 自動修復失敗,已升級人工介入</b>"
assert await gateway.append_incident_update("INC-DEDUP", status_line) is True
assert await gateway.append_incident_update("INC-DEDUP", status_line) is True
assert [method for method, _ in sent_requests] == [
"editMessageReplyMarkup",
"sendMessage",
]
class TestSentryErrorMessage:
"""測試 Sentry 錯誤訊息"""
def test_sentry_error_format_basic(self):
"""測試基本 Sentry 錯誤格式"""
msg = SentryErrorMessage(
error_id="SENTRY-abc123",
error_type="TypeError",
error_message="Cannot read property 'x' of undefined",
service_name="awoooi-api",
file_location="src/api/v1/incidents.py:123",
)
result = msg.format()
assert "🐛" in result
assert "SENTRY ERROR" in result
assert "TypeError" in result
assert "awoooi-api" in result
assert len(result) <= 900
def test_sentry_error_with_stack_trace(self):
"""測試含 Stack Trace 的 Sentry 錯誤"""
msg = SentryErrorMessage(
error_id="SENTRY-xyz789",
error_type="ValueError",
error_message="Invalid input",
service_name="awoooi-web",
file_location="src/components/App.tsx:45",
occurrence_count=15,
affected_users=3,
first_seen="10 分鐘前",
stack_trace=[
"incidents.py:123 in get_incident",
"service.py:45 in fetch_data",
"db.py:89 in query",
],
)
result = msg.format()
assert "發生次數: <code>15</code>" in result
assert "影響用戶: <code>3</code>" in result
assert "Stack Trace" in result
class TestResourceWarnMessage:
"""測試資源告警訊息"""
def test_resource_warn_format_basic(self):
"""測試基本資源告警格式"""
msg = ResourceWarnMessage(
resource_id="RES-20260329-0001",
pod_name="awoooi-api-7d4b8c9f5-abc12",
namespace="awoooi-prod",
cpu_percent=92.5,
memory_percent=78.0,
disk_percent=45.0,
)
result = msg.format()
assert "⚠️" in result
assert "資源告警" in result
assert "CPU: 🔴" in result # 92.5% > 90%
assert "Memory: 🟡" in result # 78% >= 70%
assert "Disk: 🟢" in result # 45% < 70%
assert len(result) <= 900
def test_resource_warn_with_limits(self):
"""測試含限制資訊的資源告警"""
msg = ResourceWarnMessage(
resource_id="RES-20260329-0002",
pod_name="test-pod",
cpu_percent=85.0,
cpu_limit="500m",
memory_percent=60.0,
memory_limit="512Mi",
)
result = msg.format()
assert "(limit: 500m)" in result
assert "(limit: 512Mi)" in result
class TestRepairReportMessage:
"""測試自動修復報告"""
def test_repair_report_format_basic(self):
"""測試基本修復報告格式"""
msg = RepairReportMessage(
report_date="2026-03-29",
total_repairs=12,
success_count=10,
failure_count=2,
saved_minutes=45,
)
result = msg.format()
assert "🔧" in result
assert "自動修復報告" in result
assert "總修復次數: <code>12</code>" in result
assert "成功: ✅ <code>10</code> (83%)" in result
assert len(result) <= 900
def test_repair_report_with_top_issues(self):
"""測試含 Top 問題的修復報告"""
msg = RepairReportMessage(
report_date="2026-03-29",
total_repairs=12,
success_count=10,
failure_count=2,
top_issues=[
("Pod CrashLoopBackOff", 5),
("OOM Killed", 4),
("Image Pull Failed", 3),
],
ai_cost_gemini=0.0234,
ai_tokens_total=1823,
)
result = msg.format()
assert "Top 3 問題" in result
assert "Pod CrashLoopBackOff" in result
assert "Gemini: $0.0234" in result
class TestDailySummaryMessage:
"""測試每日摘要"""
def test_daily_summary_format_basic(self):
"""測試基本每日摘要格式"""
msg = DailySummaryMessage(
summary_date="2026-03-29",
alert_total=45,
alert_critical=2,
alert_medium=18,
alert_low=25,
)
result = msg.format()
assert "📊" in result
assert "每日摘要" in result
assert "總數: <code>45</code>" in result
assert "Critical: <code>2</code>" in result
assert len(result) <= 900
def test_daily_summary_with_full_stats(self):
"""測試完整統計的每日摘要"""
msg = DailySummaryMessage(
summary_date="2026-03-29",
alert_total=45,
auto_repair_count=30,
manual_approval_count=10,
ignored_count=5,
api_availability=99.95,
ai_cost=0.15,
budget_remaining=9.85,
)
result = msg.format()
assert "自動修復: <code>30</code>" in result
assert "API: <code>99.95%</code>" in result
assert "預算剩餘: $9.85" in result
class TestDeploySuccessMessage:
"""測試部署成功訊息"""
def test_deploy_success_format_basic(self):
"""測試基本部署成功格式"""
msg = DeploySuccessMessage(
commit_sha="abc1234567",
triggered_by="ogt",
environment="Production",
)
result = msg.format()
assert "" in result
assert "部署成功" in result
assert "abc12345" in result # 前 8 字元
assert "@ogt" in result
assert len(result) <= 900
def test_deploy_success_with_e2e(self):
"""測試含 E2E 結果的部署成功"""
msg = DeploySuccessMessage(
commit_sha="abc1234567",
triggered_by="ogt",
api_version="v1.2.3",
web_version="v1.2.3",
duration_seconds=225, # 3m 45s
e2e_passed=26,
e2e_total=26,
health_check_passed=True,
)
result = msg.format()
assert "v1.2.3" in result
assert "3m 45s" in result
assert "✅ 26/26 PASSED" in result
class TestRateLimitMessage:
"""測試 API 限額警告"""
def test_rate_limit_format_basic(self):
"""測試基本限額警告格式"""
msg = RateLimitMessage(
provider="gemini",
daily_usage=450,
daily_limit=500,
token_usage=85000,
token_limit=100000,
cost_usd=0.08,
)
result = msg.format()
assert "⚠️" in result
assert "API 限額警告" in result
assert "GEMINI API" in result
assert "450/500" in result
assert "(90%)" in result
assert len(result) <= 900
def test_rate_limit_with_suggestions(self):
"""測試含建議的限額警告"""
msg = RateLimitMessage(
provider="openai",
daily_usage=90,
daily_limit=100,
suggestions=[
"考慮切換到 Ollama 優先",
"或增加每日限額",
],
reset_time="明日 00:00",
)
result = msg.format()
assert "建議" in result
assert "切換到 Ollama" in result
assert "明日 00:00" in result