feat(phase6-9): Complete modular architecture and Agent Teams
Phase 6.4 - Modular Architecture: - Add lewooogo-brain adapters for LLM providers - Add lewooogo-data dual memory (Redis + PostgreSQL) - Implement consensus engine for multi-agent decisions - Add incident memory service for historical context Phase 9 - Agent Teams (Claude Agent SDK): - Add base agent class with Claude Sonnet 4 integration - Implement action planner, blast radius, and security agents - Add agent API endpoints and proposal workflow - Integrate ADR-009 OpenClaw Agent Teams architecture DevOps & CI/CD: - Add GitHub Actions CI/CD workflows (ci.yaml, cd.yaml) - Add pre-commit hooks and secrets baseline - Add docker-compose for local development - Update Kubernetes network policies Frontend Improvements: - Add auto-healing error boundary component - Update i18n messages for agent features - Enhance dual-state incident card with execution feedback Documentation: - Add 7 ADRs covering MCP, design system, architecture decisions - Update ARCHITECTURE_MEMORY.md with modular design - Add GLOBAL_RULES.md and SOUL.md for project identity Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
252
scripts/test-k8s-executor.js
Normal file
252
scripts/test-k8s-executor.js
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* AWOOOI 自動化 QA - K8s Executor 端對端測試
|
||||
* ==========================================
|
||||
* Phase 3: 驗證 K8s 執行器實際運作
|
||||
*
|
||||
* 測試流程:
|
||||
* 1. 檢查 K8s 叢集連線
|
||||
* 2. 創建/驗證 sandbox namespace
|
||||
* 3. 部署測試 Pod
|
||||
* 4. 發射告警 → 創建 Approval
|
||||
* 5. 模擬簽核 (Multi-Sig)
|
||||
* 6. 驗證 Pod 被實際重啟 (AGE 歸零)
|
||||
*
|
||||
* 用法: node scripts/test-k8s-executor.js
|
||||
*/
|
||||
|
||||
const http = require('http')
|
||||
const { execSync } = require('child_process')
|
||||
|
||||
const API_URL = 'http://localhost:8000'
|
||||
const NAMESPACE = 'awoooi-sandbox'
|
||||
const TEST_POD_NAME = 'qa-test-pod'
|
||||
const KUBECONFIG = '/Users/ogt/awoooi/apps/api/k3s-prod.yaml'
|
||||
|
||||
// ANSI Colors
|
||||
const GREEN = '\x1b[32m'
|
||||
const RED = '\x1b[31m'
|
||||
const YELLOW = '\x1b[33m'
|
||||
const CYAN = '\x1b[36m'
|
||||
const RESET = '\x1b[0m'
|
||||
|
||||
function log(status, message) {
|
||||
const icon = status === 'pass' ? `${GREEN}✓${RESET}` :
|
||||
status === 'fail' ? `${RED}✗${RESET}` :
|
||||
status === 'info' ? `${CYAN}→${RESET}` : `${YELLOW}?${RESET}`
|
||||
console.log(`${icon} ${message}`)
|
||||
}
|
||||
|
||||
function kubectl(cmd) {
|
||||
try {
|
||||
return execSync(`KUBECONFIG=${KUBECONFIG} kubectl ${cmd}`, { encoding: 'utf-8', timeout: 30000 }).trim()
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function httpRequest(method, path, body = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(path, API_URL)
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port,
|
||||
path: url.pathname,
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
let data = ''
|
||||
res.on('data', (chunk) => (data += chunk))
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve({ status: res.statusCode, data: JSON.parse(data) })
|
||||
} catch (e) {
|
||||
resolve({ status: res.statusCode, data: data })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
req.on('error', reject)
|
||||
if (body) req.write(JSON.stringify(body))
|
||||
req.end()
|
||||
})
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n' + '═'.repeat(55))
|
||||
console.log(' AWOOOI Phase 3 QA - K8s Executor 端對端測試')
|
||||
console.log('═'.repeat(55) + '\n')
|
||||
|
||||
// Step 1: Check K8s cluster connection
|
||||
log('info', '步驟 1: 檢查 K8s 叢集連線...')
|
||||
const version = kubectl('version --client -o json')
|
||||
if (version) {
|
||||
log('pass', 'kubectl 可用')
|
||||
} else {
|
||||
log('fail', 'kubectl 不可用,請確認 PATH')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const clusterInfo = kubectl('cluster-info 2>/dev/null')
|
||||
if (clusterInfo && clusterInfo.includes('running')) {
|
||||
log('pass', 'K8s 叢集連線正常')
|
||||
} else {
|
||||
log('fail', 'K8s 叢集連線失敗')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Step 2: Create sandbox namespace
|
||||
log('info', `步驟 2: 創建 ${NAMESPACE} namespace...`)
|
||||
const nsExists = kubectl(`get namespace ${NAMESPACE} 2>/dev/null`)
|
||||
if (!nsExists) {
|
||||
kubectl(`create namespace ${NAMESPACE}`)
|
||||
log('pass', `Namespace ${NAMESPACE} 已創建`)
|
||||
} else {
|
||||
log('info', `Namespace ${NAMESPACE} 已存在`)
|
||||
}
|
||||
|
||||
// Step 3: Deploy test pod
|
||||
log('info', '步驟 3: 部署測試 Pod...')
|
||||
const podManifest = `
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: ${TEST_POD_NAME}
|
||||
namespace: ${NAMESPACE}
|
||||
labels:
|
||||
app: qa-test
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- containerPort: 80
|
||||
`
|
||||
// Delete existing pod first
|
||||
kubectl(`delete pod ${TEST_POD_NAME} -n ${NAMESPACE} --ignore-not-found=true`)
|
||||
|
||||
// Create new pod
|
||||
try {
|
||||
execSync(`echo '${podManifest}' | KUBECONFIG=${KUBECONFIG} kubectl apply -f -`, { encoding: 'utf-8' })
|
||||
log('pass', `Pod ${TEST_POD_NAME} 已部署`)
|
||||
} catch (err) {
|
||||
log('fail', `Pod 部署失敗: ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Wait for pod to be ready
|
||||
log('info', '等待 Pod 就緒...')
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const status = kubectl(`get pod ${TEST_POD_NAME} -n ${NAMESPACE} -o jsonpath='{.status.phase}'`)
|
||||
if (status === 'Running') {
|
||||
log('pass', 'Pod 狀態: Running')
|
||||
break
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1000))
|
||||
}
|
||||
|
||||
// Record initial pod creation time
|
||||
const initialCreationTime = kubectl(`get pod ${TEST_POD_NAME} -n ${NAMESPACE} -o jsonpath='{.metadata.creationTimestamp}'`)
|
||||
log('info', `初始創建時間: ${initialCreationTime}`)
|
||||
|
||||
// Step 4: Fire custom alert targeting our test pod
|
||||
log('info', '步驟 4: 發射測試告警...')
|
||||
try {
|
||||
const alertPayload = {
|
||||
alert_type: 'k8s_pod_crash',
|
||||
severity: 'critical',
|
||||
source: 'awoooi-qa-test',
|
||||
target_resource: TEST_POD_NAME,
|
||||
namespace: NAMESPACE,
|
||||
message: `[QA TEST] Pod ${TEST_POD_NAME} CrashLoopBackOff - needs restart`,
|
||||
metrics: {
|
||||
restart_count: 5,
|
||||
cpu_percent: 95,
|
||||
test: true
|
||||
}
|
||||
}
|
||||
|
||||
const { status, data } = await httpRequest('POST', '/api/v1/webhooks/alerts', alertPayload)
|
||||
if (status === 200 || status === 201) {
|
||||
log('pass', `告警已發射,Approval ID: ${data.approval_id?.slice(0, 8) || data.id?.slice(0, 8)}...`)
|
||||
} else {
|
||||
log('fail', `告警發射失敗: ${JSON.stringify(data)}`)
|
||||
}
|
||||
} catch (err) {
|
||||
log('fail', `告警發射失敗: ${err.message}`)
|
||||
}
|
||||
|
||||
// Step 5: Get approval and sign it
|
||||
log('info', '步驟 5: 取得並簽核 Approval...')
|
||||
await new Promise(r => setTimeout(r, 1000)) // Wait for approval to be created
|
||||
|
||||
let approvalId = null
|
||||
try {
|
||||
const { data } = await httpRequest('GET', '/api/v1/approvals/pending')
|
||||
if (data.approvals && data.approvals.length > 0) {
|
||||
approvalId = data.approvals[0].id
|
||||
log('info', `找到 Approval: ${approvalId.slice(0, 8)}...`)
|
||||
|
||||
// Get required signatures
|
||||
const requiredSigs = data.approvals[0].required_signatures || 2
|
||||
|
||||
// Sign with multiple users
|
||||
for (let i = 1; i <= requiredSigs; i++) {
|
||||
const signResult = await httpRequest('POST', `/api/v1/approvals/${approvalId}/sign`, {
|
||||
signer_id: `qa-signer-${i}`,
|
||||
signer_name: `QA Signer ${i}`,
|
||||
comment: `K8s executor test signature ${i}`,
|
||||
})
|
||||
|
||||
if (signResult.status === 200) {
|
||||
log('pass', `簽核 ${i}/${requiredSigs} 成功`)
|
||||
} else if (signResult.status === 409) {
|
||||
log('info', `簽核 ${i} 跳過 (重複)`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log('fail', '無待簽核項目')
|
||||
}
|
||||
} catch (err) {
|
||||
log('fail', `簽核失敗: ${err.message}`)
|
||||
}
|
||||
|
||||
// Step 6: Wait and verify pod was restarted
|
||||
log('info', '步驟 6: 等待 K8s 執行器執行...')
|
||||
await new Promise(r => setTimeout(r, 5000)) // Give executor time to run
|
||||
|
||||
const finalCreationTime = kubectl(`get pod ${TEST_POD_NAME} -n ${NAMESPACE} -o jsonpath='{.metadata.creationTimestamp}' 2>/dev/null`)
|
||||
|
||||
console.log('\n' + '─'.repeat(55))
|
||||
console.log(' 驗證結果')
|
||||
console.log('─'.repeat(55))
|
||||
|
||||
if (!finalCreationTime) {
|
||||
log('info', 'Pod 已被刪除 (executor 執行 delete pod)')
|
||||
log('pass', '✨ K8s Executor 執行成功!')
|
||||
} else if (finalCreationTime !== initialCreationTime) {
|
||||
log('pass', `Pod 創建時間已更新: ${finalCreationTime}`)
|
||||
log('pass', '✨ K8s Executor 執行成功!(Pod 重啟)')
|
||||
} else {
|
||||
log('info', `Pod 創建時間未變: ${finalCreationTime}`)
|
||||
log('info', '可能原因: 執行器正在處理或 kubeconfig 權限問題')
|
||||
|
||||
// Check API logs for execution status
|
||||
log('info', '檢查後端日誌...')
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
log('info', '清理測試資源...')
|
||||
kubectl(`delete pod ${TEST_POD_NAME} -n ${NAMESPACE} --ignore-not-found=true`)
|
||||
|
||||
console.log('\n' + '═'.repeat(55))
|
||||
console.log(' Phase 3 K8s Executor 測試完成')
|
||||
console.log('═'.repeat(55) + '\n')
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
log('fail', `測試失敗: ${err.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
Reference in New Issue
Block a user