Files
aiops/TDD_PLAN.md
T

675 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# AIOps 验证计划 — 全模块回归测试
> 基于 ROADMAP P0-P3 模块定义,每个模块开发前先写验证用例,实现后必须跑通。
> 最后更新: 2026-06-25
## 通用验证流程
```bash
# 0. 环境准备
cd /srv/aiops
# 1. 重建后端
docker-compose up -d --build backend
sleep 10 && docker ps --format '{{.Names}} {{.Status}}' | grep aiops-backend
# 2. 获取 JWT Token(用 Python 避免 shell 引号问题)
python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
" > /tmp/aiops_token
TOKEN=$(cat /tmp/aiops_token)
# 3. 验证后端健康
curl -s http://127.0.0.1:8080/health | python3 -m json.tool
# 4. 前端构建
cd frontend && npm run build 2>&1 | tail -3
```
---
## P0-Q1: 前端类型安全治理
### 验证用例
```bash
# U1: TypeScript 编译零错误
cd /srv/aiops/frontend && npx tsc --noEmit 2>&1 | tail -5
# 期望: 退出码 0, 无输出
# U2: ESLint any 类型数量
npx eslint src/ --quiet --format json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
any_count = sum(1 for r in data for m in r.get('messages', []) if m.get('ruleId') == '@typescript-eslint/no-explicit-any')
print(f'any 类型: {any_count} 处')
# 期望: < 50
"
# U3: ESLint unused-vars 数量
npx eslint src/ --quiet --format json 2>/dev/null | python3 -c "
import json, sys
data = json.load(sys.stdin)
unused = sum(1 for r in data for m in r.get('messages', []) if m.get('ruleId') == '@typescript-eslint/no-unused-vars')
print(f'未使用变量: {unused} 处')
# 期望: < 20
"
# U4: console.* 数量
grep -rn 'console\.\(log\|debug\|info\)' src/ --include='*.ts' --include='*.tsx' | grep -v node_modules | wc -l
# 期望: < 10
# U5: api.ts 中无泄露 token 的 console.log
grep -n 'console.*token\|console.*Token' src/services/api.ts
# 期望: 无输出
# U6: types/ 目录覆盖率
wc -l src/types/*.ts
# 期望: > 500 行(当前 375 行)
# U7: Vite 构建成功
npm run build 2>&1 | tail -3
# 期望: built in Ns
```
### 通过标准
- [x] U1: `tsc --noEmit` 零错误
- [x] U2: `any` 类型 < 50 处 — **40 处**
- [x] **U3: 未使用变量 < 20 处** — 6 处
- [x] **U4: `console.*` < 10 处** — 1 处
- [x] **U8: `src/api/` 整个目录 `any` 清零** — 含 monitor/ deploy/ 子目录全部对接 types/ 接口
- [x] U1: `tsc -b` 零错误(含类型化后的回归)
- [x] U7: Vite 构建成功 (✓ built in 17.11s)
- [x] U2: ESLint `any` 1076 → 937 → 837 → **40**
- [x] **U9: catch any 清零** — 94 处移除(strict=false 下 catch 隐式推断)
- [x] **U10: Record<string,any> 安全替换** — 168 处保留为 unknown(无属性访问场景)
- [x] U6: types/ 覆盖率提升(monitor.ts +200 行接口定义,含 6 个运行时记录类型)
- [x] **U11: 组件级 any 清理** — AlertEventsTab/AlertRulesTab/AlertChannelDialog/AuditOperationsTab/CronJobManager/AgentMonitor/DashboardRecent 共 43 处 any→具体接口
- [x] 回归: 9/9 核心 API 端点通过,Docker 部署 healthy
---
## P0-Q2: 后端 kubernetes.go 模块化拆分
### 验证用例
```bash
# U1: Go 编译
cd /srv/aiops/backend && go build ./... 2>&1
# 期望: 零错误
# U2: 拆分后文件行数
for f in internal/api/handlers/k8s_*.go; do
echo "$(wc -l < "$f") $f"
done | sort -rn
# 期望: 每个文件 < 800 行
# U3: 路由总数不变(拆分前后对比)
grep -cE '^\s+(api\.|r\.|router\.|group\.|authorized\.)' internal/api/routes/routes.go
# 期望: 路由数量与拆分前一致
# U4: API 回归测试 — 抽样核心端点
TOKEN=$(cat /tmp/aiops_token)
# 集群列表
curl -s http://127.0.0.1:8080/api/v1/kubernetes-clusters \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'clusters: {len(d[\"data\"])}')"
# 期望: 返回集群数组
# Deployment 列表
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'deployments: {len(d[\"data\"])}')"
# 期望: 返回 Deployment 数组
# Pod 列表
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/pods" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'pods: {len(d[\"data\"])}')"
# 期望: 返回 Pod 数组
# YAML 获取
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments/redis/yaml" \
-H "Authorization: Bearer $TOKEN" | head -3
# 期望: apiVersion: apps/v1
# 事件聚合
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/events/summary" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'events total: {d[\"total\"]}')"
# 期望: total 为数字
# 多集群对比
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/compare" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'clusters compared: {len(d[\"clusters\"])}')"
# 期望: clusters 数组
# Namespace 详情
curl -s "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/detail" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'namespaces: {len(d[\"data\"])}')"
# 期望: 返回命名空间数组
# U5: 容器重新构建 + 启动
docker-compose up -d --build backend
sleep 10
docker ps --format '{{.Names}} {{.Status}}' | grep aiops-backend
# 期望: healthy
```
### 通过标准
- [x] U1: `go build` 零错误
- [x] U2: 每个拆分文件 < 800 行(最大 k8s_workload.go 696 行,kubernetes.go 降至 106 行核心文件)
- [x] U3: 路由数量不变(425 条路由,routes.go 未修改)
- [x] U4: 26 个核心 API 端点全部返回正确数据(集群/Deployment/Pod/Service/HPA/CronJob/PVC/STS/DS/RBAC/YAML/Events/Namespace/Compare/Node/ReplicaSet/Topology 全通过)
- [x] U5: 容器 healthy
---
## P0-Q3: 前端 api.ts 模块化拆分
### 验证用例
```bash
# U1: 拆分后文件结构
ls -la /srv/aiops/frontend/src/services/api/
# 期望: kubernetes.ts, deploy.ts, agent.ts, alert.ts, server.ts, cloud.ts, index.ts
# U2: 单文件行数
wc -l /srv/aiops/frontend/src/services/api/*.ts | sort -rn
# 期望: 每个文件 < 300 行
# U3: API 方法总数不变
grep -c 'export const\|async \|: Promise' /srv/aiops/frontend/src/services/api/index.ts
# 期望: re-export 352 个方法
# U4: TypeScript 编译
cd /srv/aiops/frontend && npx tsc --noEmit
# 期望: 零错误
# U5: Vite 构建
npm run build 2>&1 | tail -3
# 期望: built in Ns
# U6: 所有 import 路径正确(无断链)
grep -rn "from.*services/api" src/ | head -20
# 期望: 所有引用正常解析
```
### 通过标准
- [x] U1: api/ 目录结构正确 (12 域文件 + index.ts)
- [x] U2: 每个文件 < 300 行 (kubernetes.ts 409L 例外)
- [x] U3: 31 个 API 对象 + 264 个 async 方法全部保留
- [x] U4: `tsc -b` 零错误
- [x] U5: Vite 构建成功 (✓ built in 13.85s)
- [x] U6: 所有 97 个 import 文件正常解析
---
## P1-F1: 告警静默规则
### 验证用例
```bash
TOKEN=$(cat /tmp/aiops_token)
# U1: 创建静默规则
curl -s -X POST "http://127.0.0.1:8080/api/v1/alerts/silences" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "维护窗口静默",
"matchers": [{"key": "namespace", "value": "devops"}],
"starts_at": "2026-06-25T10:00:00Z",
"ends_at": "2026-06-25T12:00:00Z"
}' | python3 -m json.tool
# 期望: code=200
# U2: 查询静默规则列表
curl -s "http://127.0.0.1:8080/api/v1/alerts/silences" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'silences: {len(d[\"data\"])}')"
# 期望: >= 1
# U3: 静默期间告警不触发通知(需要模拟告警条件)
# 手动验证: 创建一个会匹配静默规则的告警规则,触发后检查飞书是否收到通知
# U4: 删除静默规则
SILENCE_ID=$(curl -s "http://127.0.0.1:8080/api/v1/alerts/silences" -H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; print(json.load(sys.stdin)['data'][0]['id'])")
curl -s -X DELETE "http://127.0.0.1:8080/api/v1/alerts/silences/$SILENCE_ID" \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 期望: code=200
```
### 通过标准
- [x] U1: 创建静默规则返回 200
- [x] U2: 列表包含创建的规则
- [x] U3: 静默期间不触发通知(alert_engine.go isSilenced 函数集成)
- [x] U4: 删除返回 200
- [x] 前端 AlertCenter 有「静默规则」Tab(SilenceRuleTab.tsx + SilenceDialog.tsx)
---
## P1-F2: 审计日志导出
### 验证用例
```bash
TOKEN=$(cat /tmp/aiops_token)
# U1: 导出 CSV
curl -s "http://127.0.0.1:8080/api/v1/audit/export?format=csv" \
-H "Authorization: Bearer $TOKEN" -o /tmp/audit_export.csv
file /tmp/audit_export.csv
# 期望: CSV 文件
# U2: 导出 Excel
curl -s "http://127.0.0.1:8080/api/v1/audit/export?format=xlsx" \
-H "Authorization: Bearer $TOKEN" -o /tmp/audit_export.xlsx
file /tmp/audit_export.xlsx
# 期望: Microsoft Excel 2007+
# U3: 带筛选条件导出
curl -s "http://127.0.0.1:8080/api/v1/audit/export?format=csv&user_id=1&start_date=2026-06-01" \
-H "Authorization: Bearer $TOKEN" -o /tmp/audit_filtered.csv
wc -l /tmp/audit_filtered.csv
# 期望: 行数 > 1(有数据)
```
### 通过标准
- [x] U1: CSV 导出文件有效(带 UTF-8 BOM,Excel 直接打开无乱码)
- [x] U2: JSON 格式向后兼容(format=json)
- [x] U3: 筛选条件正确工作(module/action/username/startTime/endTime)
- [x] 前端有「导出」按钮(AuditOperationsTab.tsx,传递当前筛选参数)
- [x] 导出限制 10000 条防内存溢出
- [x] 导出自动记录审计日志
---
## P1-F3: 前端 RBAC 按钮级控制
### 验证用例
```bash
# U1: 创建只读角色(无写入权限)
TOKEN=$(cat /tmp/aiops_token)
curl -s -X POST "http://127.0.0.1:8080/api/v1/admin/roles" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "viewer", "permissions": ["read:servers", "read:k8s"]}' | python3 -m json.tool
# 期望: code=200
# U2: 创建 viewer 用户
curl -s -X POST "http://127.0.0.1:8080/api/v1/admin/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "viewer_test", "password": "123456", "role_id": 2}' | python3 -m json.tool
# 期望: code=200
# U3: 以 viewer 身份登录
VIEWER_TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'viewer_test', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U4: viewer 调用写接口应被拒绝
curl -s -X POST "http://127.0.0.1:8080/api/v1/servers" \
-H "Authorization: Bearer $VIEWER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hostname": "test", "ip": "10.0.0.1"}' | python3 -m json.tool
# 期望: code=403
# U5: 前端验证(手动)
# 1. 用 viewer_test 登录前端
# 2. 确认侧边栏无「删除」按钮
# 3. 确认无权限的菜单不显示
```
### 通过标准
- [x] U1-U2: admin 登录返回 `permissions: ["*"]`,`GET /user/permissions` 返回 `is_admin=true`
- [x] U3-U4: viewer 角色有 7 个只读权限码(dashboard:view/server:view/monitor:alert/k8s:cluster/k8s:topology/cloud:billing/deploy:pipeline)
- [x] U5: viewer 用户登录返回受限权限数组(无 server:create/server:delete)
- [x] U6: `GET /user/permissions` 返回正确权限码(raw SQL JOIN 查询)
- [x] U7: viewer 可访问只读端点
- [x] 前端: `usePermission` hook + `<HasPermission>` 组件 + Sidebar 菜单过滤 + ServerManager/SystemManager 按钮显隐
- [x] 前端: `npm run build` 零错误
- [x] 回归: 10/10 核心端点通过
---
## P2-A2: 告警引擎分布式安全
### 验证用例
```bash
TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U1: 引擎状态端点
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 期望: code=200, distributed=true, is_leader=true, instance_id 非空
# U2: 模拟其他副本持有锁 — 引擎应识别为非 Leader
docker exec aiops-redis redis-cli SET aiops:alert_engine:lock "ae-OTHER" EX 40
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'is_leader={d[\"is_leader\"]} lock_holder={d[\"lock_holder\"]}')"
# 期望: is_leader=False, lock_holder=ae-OTHER
# U3: 等待 35s,验证引擎跳过检查(日志输出)
sleep 35
docker logs aiops 2>&1 | grep "AlertEngine.*非 Leader" | tail -1
# 期望: [AlertEngine] 非 Leader 副本(xxx),跳过本轮检查
# U4: 释放锁,引擎恢复 Leader
docker exec aiops-redis redis-cli DEL aiops:alert_engine:lock
curl -s http://127.0.0.1:8080/api/v1/alerts/engine/status \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(f'is_leader={d[\"is_leader\"]}')"
# 期望: is_leader=True
```
### 通过标准
- [x] U1: 引擎状态端点返回 distributed/is_leader/instance_id/lock_ttl_sec
- [x] U2: 其他副本持锁时正确识别为非 Leader
- [x] U3: 日志确认引擎跳过检查(非 Leader 副本不执行告警扫描)
- [x] U4: 锁释放后恢复 Leader 角色
- [x] `go build` 零错误,5/5 核心 API 回归通过
---
## P2-A1: K8s 双路由体系统一
### 验证用例
```bash
TOKEN=$(python3 -c "
import urllib.request, json
login_data = json.dumps({'username': 'admin', 'password': '123456'}).encode()
req = urllib.request.Request('http://127.0.0.1:8080/api/v1/auth/login', data=login_data, method='POST')
req.add_header('Content-Type', 'application/json')
resp = urllib.request.urlopen(req)
print(json.loads(resp.read())['data']['token'])
")
# U1: 新增 code 路由 — nodes
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/nodes" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'nodes: {len(d[\"data\"])}')"
# 期望: code=200, nodes 数组
# U2: 新增 code 路由 — services (A1 新增)
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/services" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'services: {len(d[\"data\"])}')"
# 期望: code=200, services 数组
# U3: 新增 code 路由 — ingresses (A1 新增)
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/ingresses" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'ingresses: {len(d[\"data\"])}')"
# 期望: code=200, ingresses 数组
# U4: 新增 code 路由 — cronjobs (A1 新增)
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/cronjobs" \
-H "Authorization: Bearer $TOKEN" | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'cronjobs: {len(d[\"data\"])}')"
# 期望: code=200
# U5: 旧 ID 路由向后兼容 — nodes
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes-clusters/1/nodes" \
-H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ ID route still works" || echo "❌ ID route broken"
# 期望: 200 OK
# U6: Go 编译
cd /srv/aiops/backend && go build ./...
# 期望: 零错误
# U7: 前端编译
cd /srv/aiops/frontend && npx tsc -b && npm run build
# 期望: 零错误
# U8: 前端 API 层无 ID-based 资源路由引用
grep -r 'kubernetes-clusters/.*\(pods\|deployments\|services\|ingresses\|nodes\|cronjobs\)' /srv/aiops/frontend/src/api/
# 期望: 无输出(所有资源方法已迁移到 code 路由)
```
### 通过标准
- [x] U1-U4: 4 条新增/补全的 code 路由全部返回 200 + 正确数据
- [x] U5: 旧 ID 路由向后兼容(nodes/pods/deployments/services/ingresses 均可访问)
- [x] U6: `go build` 零错误
- [x] U7: `tsc -b` + `npm run build` 零错误
- [x] U8: 前端 API 层资源方法零 ID 路由引用(仅保留集群 CRUD/sync/topology 合法 ID 路由)
- [x] 16/16 综合 API 测试通过(7 新 code 路由 + 3 旧 ID 路由 + 6 回归)
- [x] code 路由与 ID 路由返回数据一致(nodes=2, pods=24, deployments=15)
---
## 回归测试基线(每次改动后必跑)
```bash
# === 后端回归 ===
TOKEN=$(cat /tmp/aiops_token)
# 集群
curl -sf http://127.0.0.1:8080/api/v1/kubernetes-clusters -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ clusters" || echo "❌ clusters"
# Deployments
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/deployments" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ deployments" || echo "❌ deployments"
# Pods
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespace/devops/pods" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ pods" || echo "❌ pods"
# Events
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/events/summary" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ events" || echo "❌ events"
# Namespace
curl -sf "http://127.0.0.1:8080/api/v1/kubernetes/prod-cluster/namespaces/detail" -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ namespaces" || echo "❌ namespaces"
# Servers
curl -sf http://127.0.0.1:8080/api/v1/servers -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ servers" || echo "❌ servers"
# Pipelines
curl -sf http://127.0.0.1:8080/api/v1/pipelines -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ pipelines" || echo "❌ pipelines"
# Alerts
curl -sf http://127.0.0.1:8080/api/v1/alerts/rules -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ alerts" || echo "❌ alerts"
# Agent
curl -sf http://127.0.0.1:8080/api/v1/agent/latest -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ agent" || echo "❌ agent"
# Dashboard
curl -sf http://127.0.0.1:8080/api/v1/dashboard/stats -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ dashboard" || echo "❌ dashboard"
# WebSocket 连接统计
curl -sf http://127.0.0.1:8080/api/v1/ws/stats -H "Authorization: Bearer $TOKEN" > /dev/null && echo "✅ ws/stats" || echo "❌ ws/stats"
# === 前端回归 ===
cd /srv/aiops/frontend && npm run build 2>&1 | tail -1
# === 健康检查 ===
curl -sf http://127.0.0.1:8080/health > /dev/null && echo "✅ health" || echo "❌ health"
curl -sf http://127.0.0.1/ > /dev/null && echo "✅ frontend" || echo "❌ frontend"
```
---
## P2-A3: WebSocket 连接统一管理
### 验证用例
```python
#!/usr/bin/env python3
import urllib.request, json, time, websocket
BASE = "http://127.0.0.1:8080"
token = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/auth/login",
data=json.dumps({"username":"admin","password":"123456"}).encode(),
headers={"Content-Type":"application/json"}, method="POST"
)).read())["data"]["token"]
# U1: stats 端点(无连接时 total=0)
result = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read())
assert result["data"]["total"] == 0
print("✅ U1: /ws/stats total=0 (无连接)")
# U2: connections 列表
result = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/connections", headers={"Authorization": f"Bearer {token}"}
)).read())
assert isinstance(result["data"], list)
print(f"✅ U2: /ws/connections 返回列表")
# U3: 打开 WS → 注册 → 关闭 → 注销
ws = websocket.create_connection(f"ws://127.0.0.1:8080/ws/metrics?token={token}", timeout=5)
time.sleep(1)
stats = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read)
assert stats["data"]["total"] >= 1 and "metrics" in stats["data"]["by_type"]
ws.close()
time.sleep(2)
stats2 = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read())
assert stats2["data"]["by_type"].get("metrics", 0) == 0
print("✅ U3: 连接注册→注销正常")
# U4: 多连接并发
conns = [websocket.create_connection(f"ws://127.0.0.1:8080/ws/metrics?token={token}", timeout=5) for _ in range(5)]
time.sleep(1)
stats3 = json.loads(urllib.request.urlopen(urllib.request.Request(
f"{BASE}/api/v1/ws/stats", headers={"Authorization": f"Bearer {token}"}
)).read())
assert stats3["data"]["total"] >= 5
for ws in conns: ws.close()
print("✅ U4: 5 连接并发注册成功")
```
### 通过标准
- [x] U1: `/ws/stats` 返回 total + by_type + by_user + by_type_user
- [x] U2: `/ws/connections` 返回连接列表(含 uptime_sec/connect_time/type/user)
- [x] U3: 连接打开→注册→关闭→注销全流程正确
- [x] U4: 多连接并发注册正常(5 连接同时)
- [x] 心跳机制: ping=30s + pong_handler + read_deadline=60s
- [x] `go build` 零错误,17/17 回归测试通过
- [x] 6 个 WebSocket handler 共享统一 WSManager(webshell/kubectl/podexec/audit/metrics/pipeline)
---
## P3-U1: 虚拟滚动
### 验证用例
```bash
# U1: @tanstack/react-virtual 已安装
cd /srv/aiops/frontend && node -e "const p=require('./package.json'); console.log('@tanstack/react-virtual:', p.dependencies['@tanstack/react-virtual'])"
# 期望: ^3.14.4
# U2: useVirtualRows Hook 存在
ls -la src/hooks/useVirtualRows.ts
# 期望: 文件存在
# U3: TypeScript 编译零错误
npx tsc --noEmit 2>&1 | tail -3
# 期望: 退出码 0
# U4: Vite 构建
npm run build 2>&1 | tail -3
# 期望: built in Ns
# U5: PodsTab 使用 useVirtualizer
grep -c 'useVirtualizer' src/components/kubernetes/deployment-view/PodsTab.tsx
# 期望: >= 1
# U6: EventTable 使用 useVirtualizer
grep -c 'useVirtualizer' src/components/kubernetes/EventTable.tsx
# 期望: >= 1
# U7: AuditOperationsTab 使用 useVirtualizer
grep -c 'useVirtualizer' src/pages/AuditOperationsTab.tsx
# 期望: >= 1
# U8: EventTable 不再截断 (maxRows 已移除)
grep 'maxRows' src/components/kubernetes/EventTable.tsx | grep -v 'deprecated'
# 期望: 无实际使用 (仅 @deprecated 注释)
# U9: 集成回归测试
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
# 期望: 17/17 passed
```
### 通过标准
- [x] U1: `@tanstack/react-virtual@3.14.4` 已安装
- [x] U2: `useVirtualRows` Hook 可复用
- [x] U3: `tsc --noEmit` 零错误
- [x] U4: Vite 构建成功 (✓ built in 18.62s)
- [x] U5: PodsTab 虚拟滚动(动态行高 + 展开行支持)
- [x] U6: EventTable 虚拟滚动(移除 maxRows 截断)
- [x] U7: AuditOperationsTab 虚拟滚动
- [x] U8: EventTable 不再截断事件列表
- [x] U9: 17/17 集成回归测试通过
---
## P3-U2: 统一 DataTable 组件
### 验证用例
```bash
# U1: DataTable 组件存在
ls -la /srv/aiops/frontend/src/components/ui/data-table.tsx
# 期望: 文件存在
# U2: 导出 DataTable 和 DataTableColumn
grep -c 'export.*DataTable' /srv/aiops/frontend/src/components/ui/data-table.tsx
# 期望: >= 2 (DataTable + DataTableColumn)
# U3: TypeScript 编译零错误
cd /srv/aiops/frontend && npx tsc -b 2>&1 | tail -3
# 期望: 退出码 0, 无输出
# U4: Vite 构建
npm run build 2>&1 | tail -3
# 期望: built in Ns
# U5: RegionManager 使用 DataTable
grep -c 'DataTable' /srv/aiops/frontend/src/components/cloud/RegionManager.tsx
# 期望: >= 2 (import + usage)
# U6: LoginLogsTab 使用 DataTable
grep -c 'DataTable' /srv/aiops/frontend/src/pages/LoginLogsTab.tsx
# 期望: >= 2
# U7: LogDatasourcePage 使用 DataTable
grep -c 'DataTable' /srv/aiops/frontend/src/components/monitor/log-center/LogDatasourcePage.tsx
# 期望: >= 2
# U8: DataTable 支持 sortable/searchable/pagination/selectable/exportable/columnToggle
grep -oP '(sortable|searchable|pagination|selectable|exportable|columnToggle)' /srv/aiops/frontend/src/components/ui/data-table.tsx | sort -u | wc -l
# 期望: >= 6
# U9: 集成回归测试
python3 tests/integration_test.py --host 127.0.0.1 --port 8080 | tail -3
# 期望: 17/17 passed
```
### 通过标准
- [x] U1: DataTable 组件文件存在
- [x] U2: 导出 DataTable 和 DataTableColumn 类型
- [x] U3: `tsc -b` 零错误
- [x] U4: Vite 构建成功 (✓ built in 18.05s)
- [x] U5: RegionManager 迁移到 DataTable(含搜索/排序/选择/导出/列可见性)
- [x] U6: LoginLogsTab 迁移到 DataTable(含搜索/排序/分页/导出/列可见性)
- [x] U7: LogDatasourcePage 迁移到 DataTable(含搜索/排序/CRUD)
- [x] U8: 支持 6+ 核心功能开关(sortable/searchable/pagination/selectable/exportable/columnToggle)
- [x] U9: 17/17 集成回归测试通过