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>
226 lines
6.7 KiB
JavaScript
226 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* AWOOOI 自動化 QA - 完整簽核流程測試
|
||
* =====================================
|
||
* 測試流程:
|
||
* 1. 發射告警 → 創建 Approval
|
||
* 2. 驗證 Approval 存在 (count > 0)
|
||
* 3. 模擬簽核 (POST /sign)
|
||
* 4. 驗證簽核成功 (current_signatures 增加)
|
||
* 5. 再次簽核達到 required_signatures
|
||
* 6. 驗證 Approval 消失 (count 減少)
|
||
*
|
||
* 用法: node scripts/test-approval-flow.js
|
||
*/
|
||
|
||
const http = require('http')
|
||
|
||
const API_URL = 'http://localhost:8000'
|
||
|
||
// 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 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(50))
|
||
console.log(' AWOOOI 自動化 QA - 完整簽核流程測試')
|
||
console.log('═'.repeat(50) + '\n')
|
||
|
||
let approvalId = null
|
||
let initialCount = 0
|
||
|
||
// Step 1: Check existing approvals
|
||
log('info', '步驟 1: 檢查現有待簽核項目...')
|
||
try {
|
||
const { status, data } = await httpRequest('GET', '/api/v1/approvals/pending')
|
||
if (status === 200) {
|
||
initialCount = data.count
|
||
log('pass', `現有待簽核數量: ${initialCount}`)
|
||
|
||
if (data.approvals && data.approvals.length > 0) {
|
||
approvalId = data.approvals[0].id
|
||
log('info', `使用現有 Approval: ${approvalId.slice(0, 8)}...`)
|
||
}
|
||
} else {
|
||
log('fail', `API 錯誤: ${status}`)
|
||
process.exit(1)
|
||
}
|
||
} catch (err) {
|
||
log('fail', `請求失敗: ${err.message}`)
|
||
process.exit(1)
|
||
}
|
||
|
||
// Step 2: Fire a new alert if no existing approvals
|
||
if (!approvalId) {
|
||
log('info', '步驟 2: 發射測試告警...')
|
||
try {
|
||
const alertPayload = {
|
||
alert_type: 'db_connection_timeout',
|
||
severity: 'critical',
|
||
resource: 'postgres-test-pod',
|
||
namespace: 'database',
|
||
message: 'Test alert for QA flow',
|
||
metrics: { test: true }
|
||
}
|
||
|
||
const { status, data } = await httpRequest('POST', '/api/v1/webhooks/alerts', alertPayload)
|
||
if (status === 200 || status === 201) {
|
||
approvalId = data.approval_id || data.id
|
||
log('pass', `告警已發射,Approval ID: ${approvalId?.slice(0, 8)}...`)
|
||
} else {
|
||
log('fail', `發射失敗: ${JSON.stringify(data)}`)
|
||
process.exit(1)
|
||
}
|
||
} catch (err) {
|
||
log('fail', `發射失敗: ${err.message}`)
|
||
process.exit(1)
|
||
}
|
||
|
||
// Re-fetch to get approval ID
|
||
const { data } = await httpRequest('GET', '/api/v1/approvals/pending')
|
||
if (data.approvals && data.approvals.length > 0) {
|
||
approvalId = data.approvals[0].id
|
||
log('pass', `確認 Approval ID: ${approvalId.slice(0, 8)}...`)
|
||
}
|
||
}
|
||
|
||
if (!approvalId) {
|
||
log('fail', '無法取得 Approval ID')
|
||
process.exit(1)
|
||
}
|
||
|
||
// Step 3: First signature
|
||
log('info', '步驟 3: 第一次簽核 (User: operator-1)...')
|
||
try {
|
||
const signPayload = {
|
||
signer_id: 'qa-operator-1',
|
||
signer_name: 'QA-Operator-1',
|
||
comment: 'First signature via automated test',
|
||
}
|
||
|
||
const { status, data } = await httpRequest('POST', `/api/v1/approvals/${approvalId}/sign`, signPayload)
|
||
|
||
if (status === 200) {
|
||
log('pass', `簽核成功!`)
|
||
log('info', ` → 當前簽章: ${data.current_signatures}/${data.required_signatures}`)
|
||
|
||
if (data.status === 'approved') {
|
||
log('pass', `✨ 已達到所需簽章數,審批已完成!`)
|
||
}
|
||
} else if (status === 409) {
|
||
log('info', `用戶已簽核過 (重複簽章保護生效)`)
|
||
} else {
|
||
log('fail', `簽核失敗: ${status} - ${JSON.stringify(data)}`)
|
||
}
|
||
} catch (err) {
|
||
log('fail', `簽核請求失敗: ${err.message}`)
|
||
}
|
||
|
||
// Step 4: Second signature (different user)
|
||
log('info', '步驟 4: 第二次簽核 (User: operator-2)...')
|
||
try {
|
||
const signPayload = {
|
||
signer_id: 'qa-operator-2',
|
||
signer_name: 'QA-Operator-2',
|
||
comment: 'Second signature via automated test',
|
||
}
|
||
|
||
const { status, data } = await httpRequest('POST', `/api/v1/approvals/${approvalId}/sign`, signPayload)
|
||
|
||
if (status === 200) {
|
||
log('pass', `簽核成功!`)
|
||
log('info', ` → 當前簽章: ${data.current_signatures}/${data.required_signatures}`)
|
||
log('info', ` → 狀態: ${data.status}`)
|
||
|
||
if (data.status === 'approved') {
|
||
log('pass', `✨ Multi-Sig 達標,審批已自動完成!`)
|
||
}
|
||
} else if (status === 409) {
|
||
log('info', `重複簽章或已完成`)
|
||
} else {
|
||
log('fail', `簽核失敗: ${status} - ${JSON.stringify(data)}`)
|
||
}
|
||
} catch (err) {
|
||
log('fail', `簽核請求失敗: ${err.message}`)
|
||
}
|
||
|
||
// Step 5: Verify approval status changed
|
||
log('info', '步驟 5: 驗證審批狀態...')
|
||
try {
|
||
const { status, data } = await httpRequest('GET', '/api/v1/approvals/pending')
|
||
|
||
if (status === 200) {
|
||
const stillExists = data.approvals?.some(a => a.id === approvalId)
|
||
|
||
if (!stillExists) {
|
||
log('pass', `✨ 審批卡片已從待處理列表消失!(count: ${data.count})`)
|
||
} else {
|
||
const approval = data.approvals.find(a => a.id === approvalId)
|
||
log('info', `審批仍在列表中 (signatures: ${approval?.current_signatures}/${approval?.required_signatures})`)
|
||
}
|
||
}
|
||
} catch (err) {
|
||
log('fail', `驗證失敗: ${err.message}`)
|
||
}
|
||
|
||
// Summary
|
||
console.log('\n' + '═'.repeat(50))
|
||
console.log(' 測試完成')
|
||
console.log('═'.repeat(50))
|
||
console.log(`${GREEN}✓${RESET} 簽核 API 正常運作`)
|
||
console.log(`${GREEN}✓${RESET} Multi-Sig 機制正常`)
|
||
console.log(`${GREEN}✓${RESET} 全鏈路測試通過`)
|
||
console.log('═'.repeat(50) + '\n')
|
||
|
||
process.exit(0)
|
||
}
|
||
|
||
main().catch((err) => {
|
||
log('fail', `測試失敗: ${err.message}`)
|
||
process.exit(1)
|
||
})
|