# Skill 05: AWOOOI SRE & QA
# 可靠性工程與品質守護者

> **管轄範圍**: Playwright, API Testing, Health Monitoring
> **觸發條件**: 懷疑 UI 異常、API 錯誤、部署驗證

---

## 文件資訊

| 欄位 | 值 |
|------|-----|
| **版本** | v1.6 |
| **建立日期** | 2026-03-20 (台北) |
| **建立者** | Claude Code |
| **最後修改** | 2026-03-29 15:30 (台北) |
| **修改者** | Claude Code |

### 變更紀錄

| 版本 | 日期 | 執行者 | 變更內容 |
|------|------|--------|----------|
| v1.0 | 2026-03-20 | Claude Code | 初始建立 |
| v1.1 | 2026-03-24 | Claude Code | 禁止 Mock 測試鐵律 |
| v1.2 | 2026-03-25 | Claude Code | 加入文件資訊區塊 |
| v1.3 | 2026-03-26 | Claude Code | **Phase 15 觀測性測試** |
| v1.4 | 2026-03-26 | Claude Code | **Runner 殭屍進程診斷流程** |
| v1.5 | 2026-03-26 | Claude Code | **LLM 測試策略** (首席架構師審查 P3) |
| v1.6 | 2026-03-29 | Claude Code | **告警鏈路 E2E 驗證** (監控整合主計畫) |

---

## 🔴🔴 禁止 Mock 測試鐵律 (2026-03-24)

> **統帥明確指示**: 「全面禁止！！！」

### 禁止使用

```python
# ❌ 禁止
from unittest.mock import Mock, AsyncMock, MagicMock, patch
mock_service = AsyncMock()
with patch("src.services.xxx", mock_service):
    ...
```

### 允許使用

```python
# ✅ 允許: 修改 settings 配置值
from unittest.mock import patch
with patch.object(settings, "HMAC_SECRET", "test-secret"):
    ...

# ✅ 允許: 真實 HTTP 端點測試
from httpx import ASGITransport, AsyncClient
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
    response = await client.post("/api/v1/xxx", json=payload)
```

### 測試分類

| 類型 | 允許 | 說明 |
|------|------|------|
| E2E 測試 | ✅ | 撞擊真實端點 |
| 整合測試 | ✅ | 使用真實 PostgreSQL/Redis |
| Mock 單元測試 | ❌ | 全面禁止 |

### 已移除的 Mock 測試

- `test_stats_api.py` (2026-03-24)
- `test_webhook_telegram_integration.py` (2026-03-24)

---

## 核心約束 (絕對禁止人工 QA)

### 絕對禁止指令

```
❌ "請統帥開啟瀏覽器檢查..."
❌ "請按 F12 查看 Console..."
❌ "請截圖回報錯誤..."
❌ "請手動測試以下步驟..."
```

### 正確做法

```
✅ 自主撰寫 Playwright 腳本驗證
✅ 自主執行 curl/httpx 測試 API
✅ 自主擷取 kubectl logs 分析錯誤
✅ 自主產出驗證報告表格
```

---

## 🔴 交付前絕對硬性規定 (憲法條款 14/15)

**任何宣稱「部署完成」或「功能驗收」之前，必須執行以下腳本：**

```bash
# 無頭偵察兵 - Playwright 前端視覺驗證
cd apps/web && node scripts/verify-frontend.js
```

### 驗證通過標準

| 項目 | 標準 |
|------|------|
| Console 錯誤 | **必須為 0** |
| 事件 ID (INC-*) | 建議有 (已注入告警時) |
| RPS 數據 | 建議有 (GlobalPulse 已啟用時) |

**禁止僅依賴 curl 驗收！必須親眼確認 DOM 渲染結果！**

---

## 🔴🔴🔴 告警鏈路 E2E 驗證 (2026-03-26 ADR-025)

> **血的教訓**: URL 路徑錯誤 (`webhook` vs `webhooks`) + DNS 規則錯誤，導致 2 天無 Telegram 告警

### 部署後 Smoke Test (強制)

**任何 API 或 Alertmanager 部署後必須執行：**

```bash
#!/bin/bash
# scripts/smoke-test-alert-chain.sh

API_URL="${1:-http://192.168.0.125:32334}"  # VIP (K-HA)

echo "🔔 Testing alert chain..."

RESPONSE=$(curl -s -X POST "$API_URL/api/v1/webhooks/alertmanager" \
  -H 'Content-Type: application/json' \
  -d '{
    "receiver": "smoke-test",
    "status": "firing",
    "alerts": [{
      "status": "firing",
      "labels": {"alertname": "SmokeTest", "severity": "info"},
      "annotations": {"summary": "Smoke test from CI"}
    }]
  }')

# 驗證成功
if echo "$RESPONSE" | jq -e '.success == true' > /dev/null 2>&1; then
  echo "✅ Alert chain smoke test passed"
  echo "📬 Approval ID: $(echo "$RESPONSE" | jq -r '.approval_id')"
  exit 0
else
  echo "❌ Alert chain smoke test FAILED"
  echo "$RESPONSE"
  exit 1
fi
```

### 驗證項目

| 項目 | 驗證方式 | 失敗動作 |
|------|---------|---------|
| Webhook 端點可達 | curl 收到 200 | 回滾部署 |
| Telegram 通知送達 | 檢查 message_id | 檢查 DNS/Token |
| Approval 建立成功 | approval_id 存在 | 檢查 DB 連線 |

### DNS 連通性檢查

```bash
# 從 Pod 內測試
kubectl exec -n awoooi-prod deployment/awoooi-api -- \
  python -c "import socket; print(socket.gethostbyname('api.telegram.org'))"
```

**失敗原因**:
- CoreDNS 上游 DNS 設定錯誤 (`127.0.0.53`)
- NetworkPolicy DNS 規則標籤不匹配

---

## Playwright 自動化規範

### 測試腳本結構

```typescript
// apps/web/tests/e2e/health-check.spec.ts
import { test, expect } from '@playwright/test';

test('健康檢查頁面無 Console Error', async ({ page }) => {
  const errors: string[] = [];

  page.on('console', (msg) => {
    if (msg.type() === 'error') {
      errors.push(msg.text());
    }
  });

  await page.goto('/zh-TW');
  await page.waitForLoadState('networkidle');

  expect(errors).toHaveLength(0);
});
```

### 強制設定 (playwright.config.ts)

```typescript
export default defineConfig({
  use: {
    // ✅ 必須啟用截圖錄影
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    trace: 'retain-on-failure',
  },
  reporter: [
    ['html', { outputFolder: 'test-results/html' }],
    ['json', { outputFile: 'test-results/results.json' }],
  ],
});
```

---

## API 測試規範

### Health Check 驗證腳本

```bash
#!/bin/bash
# scripts/verify-health.sh

echo "=== AWOOOI 全鏈路健康檢查 ==="

# API Health
API_STATUS=$(curl -skf https://awoooi.wooo.work/api/v1/health | jq -r '.status')
echo "API: $API_STATUS"

# Web Frontend
WEB_STATUS=$(curl -skI https://awoooi.wooo.work/ | grep -c "HTTP/2 307")
echo "Web: $([ $WEB_STATUS -gt 0 ] && echo 'OK' || echo 'FAIL')"

# CORS Header
CORS=$(curl -skI -H "Origin: https://awoooi.wooo.work" https://awoooi.wooo.work/api/v1/health | grep -i "access-control-allow-origin")
echo "CORS: $CORS"
```

### SSE 串流驗證

```javascript
// scripts/verify-sse.js
const EventSource = require('eventsource');

const es = new EventSource('https://awoooi.wooo.work/api/v1/dashboard/stream');

es.onmessage = (event) => {
  console.log('✅ SSE Message:', JSON.parse(event.data));
  es.close();
  process.exit(0);
};

es.onerror = (err) => {
  console.error('❌ SSE Error:', err);
  process.exit(1);
};

setTimeout(() => {
  console.error('❌ SSE Timeout');
  process.exit(1);
}, 10000);
```

---

## 雙端驗證報告格式 (強制)

### 回報統帥時必須附帶此表格:

```markdown
## 雙端驗證報告

| 項目 | 前端 | 後端 | 結果 |
|------|------|------|------|
| HTTP Status | 200 | 200 | ✅ |
| CORS Header | ✅ | ✅ | ✅ |
| Console Error | 0 | N/A | ✅ |
| Response Time | 120ms | 65ms | ✅ |
| SSL Certificate | Valid | N/A | ✅ |
```

---

## 常見診斷流程

### 1. UI 渲染異常

```bash
# Step 1: Headless 截圖
cd apps/web && pnpm exec playwright screenshot https://awoooi.wooo.work /tmp/screenshot.png

# Step 2: Console Error 擷取
pnpm exec playwright test tests/e2e/console-check.spec.ts
```

### 2. API 502/503 錯誤

```bash
# Step 1: 檢查 Pod 狀態
kubectl get pods -n awoooi-prod -o wide

# Step 2: 擷取 Pod 日誌
kubectl logs deployment/awoooi-api -n awoooi-prod --tail 50

# Step 3: 檢查 NodePort 連線
curl -v http://192.168.0.120:32334/api/v1/health
```

### 3. CORS 錯誤

```bash
# 驗證 CORS Header
curl -vI -H "Origin: https://awoooi.wooo.work" https://awoooi.wooo.work/api/v1/health 2>&1 | grep -i "access-control"
```

---

## 🚨 Worker CrashLoopBackOff 診斷 (2026-03-23 新增)

> **教訓**: Worker 連續 crash 92 次，7+ 小時無 Telegram 告警才發現

### 第一步: 檢查 Pod 狀態

```bash
ssh wooo@192.168.0.120 "kubectl get pods -n awoooi-prod"
```

**警告訊號:**
- `CrashLoopBackOff` 狀態
- `RESTARTS` 數字持續增加
- `READY` 顯示 `0/1`

### 第二步: 擷取 Crash 日誌

```bash
ssh wooo@192.168.0.120 "kubectl logs <pod-name> -n awoooi-prod --tail=50"
```

### 第三步: 常見錯誤模式

| 錯誤類型 | 日誌特徵 | 修復方式 |
|---------|---------|---------|
| **ImportError** | `cannot import name 'xxx' from 'yyy'` | 檢查函數名稱是否更名 |
| **ConnectionError** | `Redis connection refused` | 檢查 REDIS_URL 環境變數 |
| **ModuleNotFoundError** | `No module named 'xxx'` | 檢查 requirements.txt/pyproject.toml |

### 第四步: Import 名稱不匹配偵測

```bash
# 當看到 ImportError 時，檢查實際函數名稱
grep -n "def <function_name>\|async def <function_name>" <source_file>
```

**2026-03-23 案例:**
- Worker 嘗試: `from src.core.redis_client import init_redis, close_redis`
- 實際函數名: `init_redis_pool`, `close_redis_pool`
- 原因: Phase 6.1.1 重構時改名，但 worker 沒同步更新

---

## 🔔 Telegram 告警系統健康檢查 (2026-03-23 新增)

### 快速診斷

```bash
# 1. Worker 狀態
ssh wooo@192.168.0.120 "kubectl get pods -n awoooi-prod | grep worker"

# 2. 最近日誌
ssh wooo@192.168.0.120 "kubectl logs deployment/awoooi-worker -n awoooi-prod --tail=20"

# 3. Redis 連線測試 (從 API pod)
curl -s http://192.168.0.120:32334/api/v1/health | jq '.redis'
```

### 無告警時的檢查清單

| 檢查項目 | 指令 | 預期結果 |
|---------|------|---------|
| Worker Running | `kubectl get pods \| grep worker` | `1/1 Running` |
| Worker Restarts | 同上 | RESTARTS < 5 |
| Redis 連線 | `/api/v1/health` | `redis: "connected"` |
| 最近事件 | `/api/v1/incidents?limit=1` | 有資料 |

---

## ⚡ 前端 Race Condition 診斷 (2026-03-23 新增)

### 症狀

- UI 元素閃爍、消失又出現
- 操作後狀態被覆蓋
- Console 無錯誤但行為異常

### 診斷模式

**Zustand Store + Polling 競爭:**

```typescript
// 🔴 問題模式: Polling 與操作同時進行
const signApproval = async () => {
  await fetch('/api/sign')  // 操作 API
  // ⚠️ 此時 Polling 可能已經用舊數據覆蓋了 state
}

// ✅ 修復模式: 操作期間暫停 Polling
const signApproval = async () => {
  stopPolling()
  await fetch('/api/sign')
  setTimeout(() => startPolling(), 1000)  // 延遲恢復
}
```

### 2026-03-23 案例

- **症狀**: 簽核卡片按下後消失，然後所有卡片消失，又全部出現
- **根因**: `fetchPending()` 每 5 秒執行，與 `signApproval()` 競爭
- **修復**: `approval.store.ts` - 簽核期間暫停 Polling

---

## 🚨 Dry-Run 先行原則 (2026-03-23 首席架構師指令)

> **目的**: 對於破壞性操作，事後驗證來不及 - 必須事前模擬

### 鐵律 1: K8s 變更必須先 Dry-Run

```bash
# ✅ 正確: 先 dry-run 確認變更範圍
kubectl apply -f k8s/awoooi-prod/deployment.yaml --dry-run=client
kubectl apply -f k8s/awoooi-prod/deployment.yaml --dry-run=server  # 更嚴格

# 確認輸出無誤後，才真正執行
kubectl apply -f k8s/awoooi-prod/deployment.yaml

# ❌ 禁止: 直接套用到 Prod
kubectl apply -f k8s/awoooi-prod/deployment.yaml  # 沒有先 dry-run
```

### 鐵律 2: 資料庫變更必須先預覽

```sql
-- ✅ 正確: 先用 SELECT 確認影響範圍
SELECT COUNT(*) FROM incidents WHERE created_at < '2025-01-01';
-- 確認數量合理後才執行
DELETE FROM incidents WHERE created_at < '2025-01-01';

-- ❌ 禁止: 直接 DELETE/UPDATE
DELETE FROM incidents WHERE ...  # 沒有先確認影響範圍
```

### 鐵律 3: 提案給統帥前必須附帶 Dry-Run 結果

```markdown
## Tier 2/3 提案模板

### 擬執行指令
kubectl rollout restart deployment/awoooi-api -n awoooi-prod

### Dry-Run 結果
deployment.apps/awoooi-api restarted (dry-run)

### 影響範圍
- 2 個 API Pod 會滾動重啟
- 預計停機時間: 0 秒 (Rolling Update)

### 回滾方案
kubectl rollout undo deployment/awoooi-api -n awoooi-prod
```

### 檢查清單 (向統帥提案前)

| 檢查項目 | 必須附帶 |
|---------|---------|
| K8s YAML 變更 | `--dry-run=client` 輸出 |
| kubectl delete | 影響的 Pod/資源清單 |
| 資料庫變更 | SELECT COUNT 結果 |
| Image 更新 | 新舊 Image Tag 對比 |

---

## 👁️ Phase 15 觀測性測試 (2026-03-26)

> **Phase 15 全部完成**: Langfuse + Trace Context + Deep Linking

### 觀測系統健康檢查

```bash
# SignOz (Traces)
curl -f http://192.168.0.188:3301/api/v1/services | jq '.data[] | .serviceName'

# Langfuse (LLMOps)
curl -f http://192.168.0.110:3100/api/health

# Sentry (Errors)
curl -f http://192.168.0.110:9000/api/0/internal/health/
```

### Trace Context 驗證

```python
# 測試 Deep Linking URL 生成
from src.core.deep_linking import DeepLinking

# 應返回有效 URL
assert DeepLinking.signoz_trace_url("abc123") == "http://192.168.0.188:3301/trace/abc123"
assert DeepLinking.langfuse_trace_url("lf-123") != ""
assert DeepLinking.sentry_issue_url("456") != ""
```

### Trace Context 傳遞測試

```python
from src.core.telemetry import get_trace_context, restore_trace_context

# 1. 無 active span 時返回 None
assert get_trace_context() is None

# 2. restore 容錯測試
with restore_trace_context(None) as span:
    assert span is not None  # 應建立新 span

with restore_trace_context({"trace_id": "", "span_id": ""}) as span:
    assert span is not None  # 空值容錯
```

### 觀測系統三報表

```markdown
## 觀測系統狀態報告

| 系統 | URL | 狀態 |
|------|-----|------|
| SignOz | http://192.168.0.188:3301 | ✅ |
| Langfuse | http://192.168.0.110:3100 | ✅ |
| Sentry | http://192.168.0.110:9000 | ✅ |
| Deep Linking | N/A (內建) | ✅ |
```

---

---

## 🚨 Runner 殭屍進程診斷 (2026-03-26 新增)

> **問題**: CI/CD Workflow 反覆失敗，錯誤訊息變化多端
> **Memory**: `feedback_runner_zombie_process.md`

### 診斷流程

#### Step 1: 識別症狀

| 錯誤訊息 | 可能原因 |
|---------|---------|
| `Missing file at path: _runner_file_commands/set_output_*` | 權限問題 (sudo 造成) |
| `File already exists: _diag/pages/*.log` | 殭屍進程未清理 |
| `TaskAgentSessionConflictException` | 多個 Runner.Listener |
| `terminal prompts disabled` | Git Auth Token 讀取失敗 |

#### Step 2: 診斷指令

```bash
# 檢查殭屍進程
ps aux | grep -E 'Runner.Listener|Runner.Worker' | grep -v grep

# 檢查 Session 衝突
tail -50 ~/actions-runner-awoooi-2/_diag/Runner_*.log | grep -i conflict

# 檢查目錄權限 (應為 wooo:wooo)
ls -la ~/actions-runner-awoooi*/_work/_temp/
```

#### Step 3: 判斷處理層級

| 情況 | 層級 | 動作 |
|------|------|------|
| 單純暫存檔案衝突 | Tier 1 | 等待 Workflow 自動重試 |
| 權限問題 | Tier 2 | 通報統帥，執行 chown 修復 |
| 殭屍進程 | Tier 2 | 通報統帥，執行 pkill 清理 |
| 服務完全卡死 | Tier 3 | 統帥親自處理服務重啟 |

### 完整修復 SOP (Tier 2)

```bash
# 詳見 Skill 04 - Runner 殭屍進程修復
# 或參考 Memory: feedback_runner_zombie_process.md
```

---

## 🧠 LLM 測試策略 (2026-03-26 首席架構師審查)

> **背景**: LLM 測試天生非確定性，需特殊處理確保 CI 穩定
> **ADR**: ADR-018-llm-testing-strategy.md (Deferred - 採用方案 A)

### 確定性參數 (必須)

```python
# ✅ 所有 LLM 測試必須使用確定性參數
response = await client.post(
    f"{OLLAMA_URL}/api/chat",
    json={
        "model": model,
        "messages": messages,
        "stream": False,
        "options": {
            "temperature": 0.0,  # 🔴 確定性輸出
            "seed": 42,          # 🔴 可重現性
        },
    },
)
```

### CI 分層策略

| 層級 | Workflow | 執行時間 | 包含測試 |
|------|----------|----------|----------|
| **Fast CI** | `ci.yaml` | ~3 min | Lint, Unit, Integration |
| **Nightly LLM** | `nightly-llm.yaml` | ~45 min | Prompt Validation, Model Regression |
| **Daily E2E** | `daily-e2e-health.yaml` | ~5 min | Health Check, K8s 驗證 |

### Ollama CPU 模式須知

> **192.168.0.188**: 純 CPU 推理 (無 GPU)，速度 ~0.45 tok/s

```python
# CPU 模式必須設定足夠長的 Timeout
TIMEOUT = 300  # 秒 (CPU 推理需 ~222-666 秒)

async with httpx.AsyncClient(timeout=TIMEOUT) as client:
    response = await client.post(...)
```

### 測試分類執行

```bash
# 快速測試 (CI 每次)
pytest apps/api/tests/ -k "not llm and not model" -v

# LLM 測試 (Nightly)
pytest apps/api/tests/test_model_regression.py -v
pytest apps/api/tests/test_prompt_validation.py -v
```

### 繁體中文輸出驗證

```python
# System Prompt 必須強調繁中
AWOOOI_SYSTEM_PROMPT = """
...
- 【重要】必須使用台灣繁體中文回應 (Traditional Chinese Taiwan)
- 禁止使用簡體中文字符 (如：与→與、说→說、这→這)
...
"""

# 驗證器範例
def validate_traditional_chinese(response: str) -> bool:
    simplified_chars = ["与", "说", "这", "为", "时"]
    return not any(c in response for c in simplified_chars)
```

### 參考

- `src/core/prompts.py`: 集中式 System Prompt (ADR-019)
- `tests/test_model_regression.py`: 模型回歸測試
- `tests/test_prompt_validation.py`: Prompt 品質測試
- `.github/workflows/nightly-llm.yaml`: Nightly LLM Workflow

---

## 🚨 告警鏈路 E2E 驗證 (2026-03-29 監控整合主計畫)

> **來源**: `docs/proposals/MONITORING_MASTER_PLAN.md` Wave A.6 + B.2
> **腳本**: `ops/scripts/alert_chain_smoke_test.py`

### 驗證項目

| 鏈路 | 端點 | 預期回應 |
|------|------|----------|
| Alertmanager | `/api/v1/webhooks/alertmanager` | 200 + incident_id |
| Sentry | `/api/v1/webhooks/sentry/error` | 200 + accepted |
| SignOz | `/api/v1/webhooks/signoz/alert` | 200 + accepted |
| Telegram | `/api/v1/telegram/status` | 200 + connected |

### CD 整合 (部署後自動驗證)

```yaml
# .github/workflows/cd.yaml
- name: Alert Chain Smoke Test
  run: python ops/scripts/alert_chain_smoke_test.py
  env:
    API_BASE: "http://awoooi-api.awoooi-prod.svc.cluster.local:8000"
```

### 失敗處置

1. Smoke Test 失敗 → CD Job 失敗
2. 直接發送 Telegram 告警 (繞過 API)
3. Incident 自動建立 (source: smoke_test)

---

## 🔴 自動修復 E2E 驗收規範 (2026-04-09)

> **背景**: 系統曾有自動修復機制卻從未成功執行（success_count 全部為 0），完整審計後修復 12 個阻斷性 Bug
> **教訓**: Playbook 匹配成功 ≠ SSH 執行成功，必須端到端驗收

### 自動修復完整鏈路

```
Alertmanager → POST /api/v1/webhooks/alertmanager
  → LLM 分析 (Nemotron) + _extract_symptoms()
    → {alert_names, affected_services, keywords}
    ⚠️ affected_services 必須取 labels.component，不能用 labels.instance (IP:port)
  → playbook_service.get_recommendations() — Jaccard 相似度
    → alert_exact_match bypass: alert_names 完全匹配時忽略 0.4 門檻
  → evaluate_auto_repair() — 查 service-registry 分級
    → BLOCK → 僅告警; AUTO → 直接執行
  → HostRepairAgent.repair(layer, component)
    → SSH: ssh -i /etc/repair-ssh/id_ed25519 wooo@192.168.0.110 repair:sentry
    → repair-bot.sh → docker compose up -d → REPAIR_OK:sentry
```

### E2E 驗收 Checklist

```bash
# Step 1: 確認 SSH binary 存在
POD=$(kubectl -n awoooi-prod get pod -l app=awoooi-api -o jsonpath='{.items[0].metadata.name}')
kubectl -n awoooi-prod exec $POD -- which ssh  # 必須有輸出

# Step 2: 確認 SSH key 可讀
kubectl -n awoooi-prod exec $POD -- ls -la /etc/repair-ssh/id_ed25519
# 預期: -r--r----- 1 root appuser ... (fsGroup=1000 生效)

# Step 3: 確認 known_hosts 有內容
kubectl -n awoooi-prod exec $POD -- wc -l /etc/repair-known-hosts/known_hosts
# 預期: 9 (hashed 格式，grep IP 會得 0 — 正常)

# Step 4: SSH 健康確認
kubectl -n awoooi-prod exec $POD -- sh -c \
  "ssh -i /etc/repair-ssh/id_ed25519 \
   -o UserKnownHostsFile=/etc/repair-known-hosts/known_hosts \
   -o StrictHostKeyChecking=yes -o BatchMode=yes -o ConnectTimeout=10 \
   wooo@192.168.0.110 health"
# 預期: REPAIR_BOT_HEALTHY:110

# Step 5: Webhook 觸發（新 fingerprint 避免去重）
curl -X POST http://192.168.0.120:32334/api/v1/webhooks/alertmanager \
  -H "Content-Type: application/json" \
  -d '{"alerts":[{"labels":{"alertname":"SentryDown","component":"sentry",
       "severity":"critical"},"fingerprint":"e2e-test-001","status":"firing",
       "startsAt":"2026-04-09T00:00:00Z","endsAt":"0001-01-01T00:00:00Z"}]}'

# Step 6: 確認 log
kubectl -n awoooi-prod logs -l app=awoooi-api --tail=50 | \
  grep -E "REPAIR_OK|auto_repair_execute_success|auto_repair_approved"
```

### Playbook symptom_pattern 要求

```json
{
  "alert_names": ["SentryDown"],       // ← alert_exact_match key，完全匹配才能 bypass
  "affected_services": ["sentry"],     // ← 必須與 labels.component 一致，不是 instance
  "severity_range": ["P1", "P2"],
  "label_patterns": {"component": "sentry"},
  "keywords": ["sentry", "9000"]
}
```

### 自動修復被阻斷的診斷方法

| 症狀 | 可能原因 | 診斷指令 |
|------|---------|---------|
| `auto_repair_approved` 沒出現 | Jaccard 分數 < 0.4 | 查 log `similarity` 欄位 |
| `can_auto_repair: false` | service-registry BLOCK/HITL | 查 `blocked_by` 欄位 |
| `ssh: command not found` | Dockerfile 缺 openssh-client | Pod exec `which ssh` |
| `Permission denied (publickey)` | known_hosts 缺少該主機 | Pod exec SSH 看錯誤訊息 |
| `Permission denied (publickey)` only on `192.168.0.188` | 188 需要 `ollama` 使用者，不是預設 `wooo` | 查 `SSH_MCP_HOST_USERS=192.168.0.188=ollama`，用 `ollama@192.168.0.188` 測 |
| `Host key is not trusted for host ...` | `/etc/ssh-mcp/known_hosts` 空檔、過期，或 Secret 已 patch 但 subPath pod 未重啟 | patch `ssh-mcp-key.known_hosts`，rollout restart API/worker，再用 `ssh_diagnose` 驗證 |
| `Load key ... Permission denied` | fsGroup 未設定 | Pod exec `ls -la /etc/repair-ssh/` |
| `Connection refused/timeout` | NetworkPolicy 封鎖 22 | Pod exec `ssh -v` 看連線過程 |
| `forbidden_shell_metachar` 且 action 是 `ssh ... '...'` | host/backup category 沒在 DecisionManager kubectl parser 前路由 SSH | 查 `alert_category` 是否為 `backup_failure`，確認 `_is_host_layer_ssh_category()` 覆蓋 |
| SSH diagnosis success but incident still needs action | `ssh_diagnose` 是只讀證據蒐集，不是修復 | 應看到 `ssh_diagnosis_collected=True` 並走 emergency/human/AI intervention |

### Telegram 按鈕 E2E 檢查 (2026-05-01)

告警卡片按鈕不是純 UI。每個按鈕都必須能在
`callback_action_spec.yaml` 找到 callback pattern，並經
`callback_dispatcher.py` 路由到實際 handler。

| 卡片/情境 | 必要按鈕 | 預期處理 |
|-----------|----------|----------|
| Approval / LLM action | approve, reject, details, ignore | 寫 approval decision、執行或拒絕、查詳情、忽略告警 |
| Auto repair unavailable / emergency | investigate, escalate/assign, rollback when applicable | 通知人工/AI Agent 介入，不可靜默 |
| Drift TYPE-4D | view diff, adopt, rollback, ignore | 看 diff、採納變更、回滾、忽略 |
| Backup / host diagnosis | restart only when rule allows, charts/logs/details, cleanup when safe | 不得提供 K8s-only repair button 當 host/backup 主動作 |
| Post-verification degraded/failed | rollback proposal, investigate, details | 不自動 rollback，需人工或 emergency AI Agent 接手 |
| SecOps authorize/isolate/block | record authorization, multi-sig gate | 不直接執行危險隔離；必須寫 Redis TTL、AOL、timeline |

Regression test target: button callback names emitted by `telegram_gateway.py`
must stay in sync with `callback_action_spec.yaml`; stale buttons are a
production bug because Telegram cards can outlive code deploys.

Provider name drift is also a ghost-button bug. `callback_action_spec.yaml`
may use friendly names (`k8s`, `ssh`), but dispatcher must normalize to actual
registered MCP providers (`kubernetes`, `ssh_host`) before `get_provider()`.
`backup_failure` cards must expose read-only diagnostics before any write
action: host disk, backup jobs, and Velero backup status.

Emergency intervention is not complete until it is queryable later. Any
auto-repair-unavailable, drift-auto-adopt-blocked, or SecOps authorization path
must write both `alert_operation_log` and `timeline_events` using existing enum
values (`APPROVAL_ESCALATED` / `USER_ACTION`) unless a migration has already
landed. Telegram-only escalation is a silent learning-loop failure.

All Telegram alert lifecycle operations must use `TelegramGateway.alert_chat_id`:
initial send, analyzing placeholder, delete, editMessageText,
editMessageReplyMarkup, CI progress, and action-result updates. Sending the
card to the SRE group but editing/deleting the DM is a ghost-button bug.

---

## 參考文檔

- `apps/web/playwright.config.ts`: Playwright 設定
- `apps/web/tests/`: E2E 測試腳本
- `scripts/`: 自動化驗證腳本
- `apps/web/src/stores/approval.store.ts`: Polling + Race Condition 修復範例
- `src/core/deep_linking.py`: **👁️ Deep Linking URL 生成器**
- `src/core/telemetry.py`: **Phase 15.2 Trace Context**
- `memory/project_phase15_langfuse.md`: **📊 Phase 15 完整記錄**
- `memory/feedback_runner_zombie_process.md`: **🚨 Runner 殭屍進程修復**
- `docs/adr/ADR-018-llm-testing-strategy.md`: **🧠 LLM 測試策略 (Deferred)**
- `docs/adr/ADR-019-system-prompt-management.md`: **📝 System Prompt 集中管理**
- `docs/adr/ADR-058-host-auto-repair-ssh-whitelist.md`: **🔴 SSH 自動修復 + Bug 修復記錄**
- `.github/workflows/nightly-llm.yaml`: **🌙 Nightly LLM 測試**
- `.github/workflows/daily-e2e-health.yaml`: **🏥 Daily E2E 健康檢查**
