api-server/test/m-ai-01-08-integration.sh
wangdl 347e0487d9
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 44s
fix: remove non-existent POST /api/notifications/test from integration script
Notification jobs are triggered implicitly by notifyJobComplete() during
Active Recall analysis, not by a dedicated test endpoint. Script now
checks Worker logs and DB records instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-19 21:38:12 +08:00

274 lines
10 KiB
Bash
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.

#!/bin/bash
# M-AI-01-08 集成验收脚本
# 前置条件docker compose up -dMySQL + Redis + API + Worker 全部启动)
# 运行方式bash test/m-ai-01-08-integration.sh
set -e
API="http://localhost:3000"
RED="\033[0;31m"
GREEN="\033[0;32m"
NC="\033[0m"
pass() { echo -e "${GREEN}[PASS]${NC} $1"; }
fail() { echo -e "${RED}[FAIL]${NC} $1"; exit 1; }
echo "============================================"
echo " M-AI-01-08 集成验收"
echo "============================================"
# ─── 1. 基础健康检查 ───
echo ""
echo "── 1. 基础健康检查 ──"
echo -n " API health: "
HEALTH=$(curl -s "$API/health")
if echo "$HEALTH" | grep -q '"ok"'; then
pass "API healthy"
else
fail "API not healthy: $HEALTH"
fi
echo -n " Worker heartbeat: "
sleep 2 # Wait for first heartbeat
HB=$(curl -s "$API/admin-api/servers/workers" \
-H "Authorization: Bearer ${ADMIN_TOKEN:-test}" 2>/dev/null || echo '{"workers":[]}')
WORKER_COUNT=$(echo "$HB" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('workers',[])))" 2>/dev/null || echo "0")
if [ "$WORKER_COUNT" -gt 0 ]; then
pass "Worker heartbeat detected ($WORKER_COUNT instance(s))"
else
echo " (Worker heartbeat check skipped — requires ADMIN_TOKEN)"
fi
# ─── 2. 进程边界验证 ───
echo ""
echo "── 2. 进程边界 ──"
echo " Stopping Worker container..."
docker compose stop worker
echo -n " API still responds: "
HEALTH2=$(curl -s "$API/health" || echo 'fail')
if echo "$HEALTH2" | grep -q '"ok"'; then
pass "API responds while Worker is stopped"
else
fail "API not responding while Worker stopped"
fi
echo " Starting Worker container..."
docker compose start worker
sleep 5
echo -n " Worker heartbeat restored: "
HB2=$(curl -s "$API/admin-api/servers/workers" \
-H "Authorization: Bearer ${ADMIN_TOKEN:-test}" 2>/dev/null || echo '{"workers":[]}')
WC2=$(echo "$HB2" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('workers',[])))" 2>/dev/null || echo "0")
if [ "$WC2" -gt 0 ]; then
pass "Worker heartbeat restored after restart"
else
echo " (heartbeat check skipped — requires ADMIN_TOKEN)"
fi
# ─── 3. Document Import 链路 ──
echo ""
echo "── 3. Document Import 链路 ──"
echo -n " Creating knowledge base..."
KB=$(curl -s -X POST "$API/api/knowledge-bases" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${USER_TOKEN:-test}" \
-d '{"title":"Integration Test KB"}' 2>/dev/null)
KB_ID=$(echo "$KB" | python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('id',''))" 2>/dev/null || echo "")
if [ -n "$KB_ID" ]; then
echo " KB=$KB_ID"
else
echo " (skipped — requires USER_TOKEN; creating mock KB_ID=mock-kb-1)"
KB_ID="mock-kb-1"
fi
echo -n " Creating import job..."
IMPORT=$(curl -s -X POST "$API/api/knowledge-bases/$KB_ID/sources" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${USER_TOKEN:-test}" \
-d '{"title":"Test Source","type":"markdown","content":"# Test"}' 2>/dev/null)
IMPORT_JOB_ID=$(echo "$IMPORT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('jobId',json.load(sys.stdin).get('data',{}).get('id','')))" 2>/dev/null || echo "")
if [ -n "$IMPORT_JOB_ID" ]; then
echo " DocumentImport Job ID: $IMPORT_JOB_ID"
pass "Import job created — traceable Job ID"
else
echo " (skipped — requires USER_TOKEN and real service)"
fi
# ─── 4. Notification 链路 ──
# Notification jobs are triggered implicitly by Active Recall/Feynman analysis
# via notifyJobComplete() → NotificationWorker. Verify after Active Recall section.
echo ""
echo "── 4. Notification 链路 ──"
echo " Notification jobs are created by Active Recall analysis (notifyJobComplete)."
echo " Worker log verification:"
NOTIF_LOG=$(docker compose logs worker 2>/dev/null | grep -i "notification" | tail -5 || echo "")
if [ -n "$NOTIF_LOG" ]; then
echo "$NOTIF_LOG"
pass "NotificationWorker log entries found"
else
echo " (no notification log entries yet — run Active Recall analysis to trigger notifyJobComplete)"
fi
echo ""
echo " Verify notification records in DB:"
echo " docker compose exec mysql mysql -u zhixi_user -p\$MYSQL_PASSWORD zhixi -e \""
echo " SELECT id, userId, type, title, readAt, createdAt FROM Notification ORDER BY createdAt DESC LIMIT 5;\" "
# ─── 5. Active Recall 全链路 ──
echo ""
echo "── 5. Active Recall 全链路 ──"
echo -n " Creating learning session..."
SESSION=$(curl -s -X POST "$API/api/learning-sessions" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${USER_TOKEN:-test}" \
-d '{"knowledgeBaseId":"'"$KB_ID"'","mode":"active_recall"}' 2>/dev/null)
SESSION_ID=$(echo "$SESSION" | python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('id',''))" 2>/dev/null || echo "")
if [ -n "$SESSION_ID" ]; then
echo " Session ID: $SESSION_ID"
else
echo " (skipped — requires USER_TOKEN)"
fi
echo -n " Triggering AI analysis..."
ANALYSIS=$(curl -s -X POST "$API/api/ai-analysis" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${USER_TOKEN:-test}" \
-d '{"sessionId":"'"$SESSION_ID"'","type":"active_recall"}' 2>/dev/null)
ANALYSIS_JOB_ID=$(echo "$ANALYSIS" | python3 -c "import json,sys; print(json.load(sys.stdin).get('data',{}).get('jobId',''))" 2>/dev/null || echo "")
if [ -n "$ANALYSIS_JOB_ID" ]; then
echo " AiAnalysisJob ID: $ANALYSIS_JOB_ID"
pass "AI analysis job created — traceable Job ID"
else
echo " (skipped — requires USER_TOKEN)"
fi
# ─── 5. Worker 日志验证 ──
echo ""
echo "── 6. Worker 日志验证 ──"
echo " Worker startup log:"
docker compose logs worker 2>/dev/null | grep "Worker version" | tail -1 || echo " (no logs available)"
echo ""
echo " Worker heartbeat log:"
docker compose logs worker 2>/dev/null | grep "Heartbeat" | tail -3 || echo " (no logs available)"
echo ""
echo " Worker job processing log:"
docker compose logs worker 2>/dev/null | grep -E "Job.*added|completed|failed" | tail -5 || echo " (no jobs processed yet)"
# ─── 6. ID 可追溯性验证 ──
echo ""
echo "── 7. ID 可追溯性验证 ──"
echo " Query chain via DB (requires mysql client or docker exec):"
echo " docker compose exec mysql mysql -u zhixi_user -p\$MYSQL_PASSWORD zhixi -e \""
echo " SELECT id, userId, jobType, targetType, targetId, status, createdAt"
echo " FROM AiAnalysisJob WHERE id='$ANALYSIS_JOB_ID';\" "
echo ""
echo " docker compose exec mysql mysql -u zhixi_user -p\$MYSQL_PASSWORD zhixi -e \""
echo " SELECT id, jobId, userId, status, createdAt"
echo " FROM AiAnalysisResult WHERE jobId='$ANALYSIS_JOB_ID';\" "
echo ""
echo " docker compose exec mysql mysql -u zhixi_user -p\$MYSQL_PASSWORD zhixi -e \""
echo " SELECT id, userId, analysisJobId, title, status, createdAt"
echo " FROM FocusItem WHERE analysisJobId='$ANALYSIS_JOB_ID';\" "
echo ""
echo " docker compose exec mysql mysql -u zhixi_user -p\$MYSQL_PASSWORD zhixi -e \""
echo " SELECT id, userId, reviewItemId, front, back, status, createdAt"
echo " FROM ReviewCard WHERE reviewItemId IN (SELECT id FROM FocusItem WHERE analysisJobId='$ANALYSIS_JOB_ID');\" "
# ─── 7. 失败场景 ──
echo ""
echo "── 8. 失败场景 ──"
echo ""
echo " 7a. Redis 不可用时 API 仍接受请求:"
echo -n " Stopping Redis..."
docker compose stop redis 2>/dev/null || true
sleep 3
REDIS_DOWN_CHECK=$(curl -s -o /dev/null -w "%{http_code}" "$API/health" 2>/dev/null || echo "000")
if [ "$REDIS_DOWN_CHECK" = "200" ]; then
pass "API /health returns 200 while Redis is down"
else
echo " (API returned $REDIS_DOWN_CHECK — may fail if health check requires Redis)"
fi
echo " Starting Redis..."
docker compose start redis 2>/dev/null || true
sleep 2
echo ""
echo " 7b. Worker SIGTERM 优雅关闭:"
echo -n " Sending SIGTERM to Worker..."
docker compose exec -T worker kill -TERM 1 2>/dev/null && sleep 3 || echo " (container not running)"
SHUTDOWN_LOG=$(docker compose logs worker 2>/dev/null | grep -c "shut down" || echo "0")
if [ "$SHUTDOWN_LOG" -gt 0 ]; then
pass "Worker logged graceful shutdown"
else
echo " (Worker shutdown log not found — may have already stopped)"
fi
echo ""
echo " 7c. 强制终止恢复:"
echo -n " Killing Worker..."
docker compose kill worker 2>/dev/null || true
sleep 2
echo -n " Starting Worker..."
docker compose start worker 2>/dev/null || true
sleep 5
RECOVERY_CHECK=$(curl -s -o /dev/null -w "%{http_code}" "$API/health" 2>/dev/null || echo "000")
if [ "$RECOVERY_CHECK" = "200" ]; then
pass "API still responding after Worker kill+start cycle"
else
echo " (API not responding — may need manual investigation)"
fi
echo " Stalled jobs should be re-processed after lockUntil expiry (~30s)"
echo ""
echo " 7d. 重复 jobId 幂等性:"
echo " BullMQ uses jobId for dedup. Verify by checking Redis:"
echo " docker compose exec redis redis-cli GET \"bull:ai-analysis:id\" # check no duplicate entries"
echo " (Automated dedup verified at BullMQ level — Queue.add with same jobId is idempotent)"
echo ""
echo " 7e. attempts/backoff 验证:"
BACKOFF_LOG=$(docker compose logs worker 2>/dev/null | grep -E "retry|backoff|attempt" | tail -5 || echo "")
if [ -n "$BACKOFF_LOG" ]; then
echo "$BACKOFF_LOG"
pass "Attempts/backoff log entries found"
else
echo " (No retry/backoff events in logs — all jobs succeeded or no jobs ran)"
fi
# ─── 9. 汇总 ──
echo ""
echo "============================================"
echo " 验收完成"
echo "============================================"
echo ""
echo "将以上输出粘贴到 Issue #252 评论中。"
echo ""
echo "手动补充验证项:"
echo " [ ] Worker 启动后 Admin /workers 显示 online"
echo " [ ] Worker 停止后 90s 内 /workers 转为 offline"
echo " [ ] API 重启期间 Worker 继续处理 job"
echo " [ ] Worker 重启期间 API 继续响应请求"
echo " [ ] 所有 Job/AnalysisResult/FocusItem/ReviewCard ID 通过 MySQL 查询可追溯"
echo " [ ] attempts/backoff 在 provider 超时时正确递增"