bare `$var` 紧贴全角()(等 → strict-locale bash(macOS 5.3)把多字节首字节并进变量名 → set -u unbound;
smoke 在 lili-mac 一贯断在 template/list 行。4 处加 ${}(tcount/VID/QSCORE/SID),语义不变、跨环境可跑。
U6 验证(staging 五业务链路,本机 curl 可达):11/12 真绿——创作 draft / 发布(feed/zones/project) /
试玩取包(vid=93112) / 广告钱包(ad/trade) / 数据回路(qualityScore=60)。唯一 FAIL=template/list count=0
= W-CLEAN 清旧四模板 + generic 桥接的设计态(总账 §1 记非回归);R3=链路就绪门(不代理 R1)成立。
follow-up: template/list assertion 按 generic 桥接更新(非本单元业务语义裁定)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
181 lines
10 KiB
Bash
Executable File
181 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# 造梦/绘境AI · staging 五链路冒烟门(deploy smoke gate)
|
||
# -----------------------------------------------------------------------------
|
||
# 用途:每次 staging 部署后跑一遍,**秒级**确认 5 条端到端闭环的关键 API 在岗、
|
||
# 鉴权/CORS/质量分回路结构正常。这是「部署后是否健康」的就绪检查(liveness/
|
||
# readiness),**不是**深度 e2e(深度 e2e 走 agent-loop orchestrator 批跑)。
|
||
#
|
||
# 设计原则:
|
||
# - 默认 **以读为主**(health/列表/feed/钱包/包体),可随时重复跑;唯一写=studio/draft 建一条
|
||
# 草稿(status 0,不发布、无下游副作用、可忽略),用于验证创作入口+鉴权+gameId 分配在岗;
|
||
# - `--deep` 才走重写路径(generate 入队触发真实生成 + 遥测事件 + 互动),会落库,按需开启;
|
||
# - 每条链路逐项 PASS/FAIL,末尾总判 + 退出码(0=全过,1=有 FAIL)——可挂 CI/部署脚本。
|
||
#
|
||
# 用法:
|
||
# bash deploy/smoke-test.sh # 默认 staging 只读冒烟
|
||
# BASE=http://100.64.0.7:48080 TOKEN=test1 bash deploy/smoke-test.sh
|
||
# bash deploy/smoke-test.sh --deep # 含写路径(生成入队+遥测事件)
|
||
#
|
||
# 约定:内网地址/测试 token 可入仓(内网非密);**真实密钥严禁写入本文件**。
|
||
# =============================================================================
|
||
set -uo pipefail
|
||
|
||
BASE="${BASE:-http://100.64.0.7:48080}" # staging 后端(mini-desktop)
|
||
TOKEN="${TOKEN:-test1}" # staging 批跑/冒烟测试态 token(种子创作者)
|
||
TENANT="${TENANT:-1}"
|
||
ORIGIN="${ORIGIN:-http://100.64.0.7:4173}" # 前端 origin(验 CORS 同源放行)
|
||
DEEP=0
|
||
[ "${1:-}" = "--deep" ] && DEEP=1
|
||
|
||
PASS=0; FAIL=0; FAILED_ITEMS=()
|
||
|
||
# 颜色(非 TTY 自动降级为纯文本)
|
||
if [ -t 1 ]; then G='\033[0;32m'; R='\033[0;31m'; Y='\033[0;33m'; B='\033[0;34m'; N='\033[0m'; else G=''; R=''; Y=''; B=''; N=''; fi
|
||
|
||
_log() { printf "%b\n" "$*"; }
|
||
_head() { printf "\n%b== %s ==%b\n" "$B" "$1" "$N"; }
|
||
|
||
# api METHOD PATH [DATA] —— 返回 "HTTP_CODE\nBODY";鉴权头正确引用(防 'Bearer test1' 空格被拆)
|
||
api() {
|
||
local method="$1" path="$2" data="${3:-}"
|
||
local url="$path"; [[ "$path" == http* ]] || url="$BASE$path"
|
||
if [ -n "$data" ]; then
|
||
curl -s -w $'\n%{http_code}' -X "$method" "$url" \
|
||
-H "Authorization: Bearer $TOKEN" -H "tenant-id: $TENANT" \
|
||
-H "Origin: $ORIGIN" -H "Content-Type: application/json" -d "$data" 2>/dev/null
|
||
else
|
||
curl -s -w $'\n%{http_code}' -X "$method" "$url" \
|
||
-H "Authorization: Bearer $TOKEN" -H "tenant-id: $TENANT" \
|
||
-H "Origin: $ORIGIN" 2>/dev/null
|
||
fi
|
||
}
|
||
|
||
# check NAME METHOD PATH MODE [EXTRA] [DATA]
|
||
# MODE=health → HTTP 200 即过(actuator)
|
||
# MODE=envelope → HTTP 200 ∧ huijing body code==0 即过
|
||
# MODE=envelope_nonempty → 再要求 data 非空(list 长度≥1 或 data 非 null)
|
||
# EXTRA:envelope_nonempty 下可传 jq 风格 python 取数表达式(默认探 data.list 或 data)
|
||
check() {
|
||
local name="$1" method="$2" path="$3" mode="$4" data="${5:-}"
|
||
local out code body
|
||
out="$(api "$method" "$path" "$data")"
|
||
code="$(printf '%s' "$out" | tail -n1)"
|
||
body="$(printf '%s' "$out" | sed '$d')"
|
||
local ok=0 detail=""
|
||
case "$mode" in
|
||
health)
|
||
[ "$code" = "200" ] && ok=1; detail="http=$code" ;;
|
||
envelope|envelope_nonempty)
|
||
if [ "$code" = "200" ]; then
|
||
# 解析 huijing 信封 {code,data};envelope_nonempty 再判 data 是否非空
|
||
local pyout
|
||
pyout="$(printf '%s' "$body" | python3 -c "
|
||
import sys,json
|
||
try: d=json.load(sys.stdin)
|
||
except Exception as e: print('ERR|parse'); sys.exit()
|
||
c=d.get('code')
|
||
data=d.get('data')
|
||
if isinstance(data,dict): n=len(data.get('list')) if isinstance(data.get('list'),list) else (1 if data else 0)
|
||
elif isinstance(data,list): n=len(data)
|
||
elif data is None: n=0
|
||
else: n=1
|
||
print('%s|%s'%(c,n))
|
||
" 2>/dev/null)"
|
||
local ecode="${pyout%%|*}" ecount="${pyout##*|}"
|
||
if [ "$ecode" = "0" ]; then
|
||
if [ "$mode" = "envelope_nonempty" ]; then
|
||
[ "${ecount:-0}" -ge 1 ] 2>/dev/null && ok=1 || ok=0
|
||
detail="code=0 count=$ecount"
|
||
else ok=1; detail="code=0"; fi
|
||
else detail="http=$code code=$ecode"; fi
|
||
else detail="http=$code"; fi ;;
|
||
esac
|
||
if [ "$ok" = "1" ]; then PASS=$((PASS+1)); _log " ${G}✓${N} $name ($detail)"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("$name [$detail]"); _log " ${R}✗${N} $name ($detail)"; fi
|
||
}
|
||
|
||
_log "${B}造梦AI staging 五链路冒烟门${N} BASE=$BASE TOKEN=${TOKEN:0:2}*** deep=$DEEP"
|
||
|
||
# ---------------- 基础设施 + 鉴权 ----------------
|
||
_head "基础设施 / 鉴权"
|
||
check "infra: actuator/health" GET /actuator/health health
|
||
check "auth: passport/me(token 有效)" GET /app-api/passport/me envelope
|
||
|
||
# ---------------- 链路① 创作 → 生成 → 预览试玩 ----------------
|
||
_head "链路① 创作→生成→预览"
|
||
# 四模板就绪(执行器 banner 同源:getTemplateList 应返 clicker/merge/idle/tycoon)
|
||
out="$(api GET /app-api/aigc/template/list)"
|
||
tcount="$(printf '%s' "$out" | sed '$d' | python3 -c "import sys,json
|
||
try: d=json.load(sys.stdin); data=d.get('data') or []; print(len(data) if isinstance(data,list) else 0)
|
||
except: print(-1)" 2>/dev/null)"
|
||
if [ "${tcount:-0}" -ge 4 ] 2>/dev/null; then PASS=$((PASS+1)); _log " ${G}✓${N} aigc/template/list(四模板就绪 count=${tcount})"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("template/list count=$tcount"); _log " ${R}✗${N} aigc/template/list(期望≥4,实=${tcount})"; fi
|
||
check "studio/draft(创建草稿拿 gameId)" POST /app-api/studio/draft envelope_nonempty '{"title":"冒烟门-clicker","templateId":"clicker"}'
|
||
|
||
# ---------------- 链路② 发布 → 审核 → 游戏流 ----------------
|
||
_head "链路② 发布→审核→游戏流"
|
||
check "feed/stream(内容池非空)" GET "/app-api/feed/stream?size=5" envelope_nonempty
|
||
check "feed/zones(分区结构)" GET /app-api/feed/zones envelope
|
||
check "project/my(创作者项目区)" GET "/app-api/project/my?pageNo=1&pageSize=5" envelope
|
||
|
||
# ---------------- 链路③ 游戏流 → 试玩 → 互动 → 分享 ----------------
|
||
_head "链路③ 试玩→互动→分享"
|
||
# 真实取包走 GET /app-api/runtime/package/{versionId}(descriptor 含 entry/manifest/sandbox);
|
||
# 注意:feed item 的 packageUrl 字段恒为 null,**不是**取包入口(runtime.ts:25 实证)。
|
||
VID="$(api GET "/app-api/feed/stream?size=1" | sed '$d' | python3 -c "
|
||
import sys,json
|
||
try:
|
||
d=json.load(sys.stdin); lst=(d.get('data') or {}).get('list') or []
|
||
print(lst[0].get('versionId','') if lst else '')
|
||
except: print('')" 2>/dev/null)"
|
||
if [ -n "$VID" ] && [ "$VID" != "None" ]; then
|
||
check "runtime/package/{versionId}(试玩取包 vid=${VID})" GET "/app-api/runtime/package/$VID" envelope
|
||
else FAIL=$((FAIL+1)); FAILED_ITEMS+=("feed 首条无 versionId"); _log " ${R}✗${N} feed 首条无 versionId(无法验取包)"; fi
|
||
|
||
# ---------------- 链路④ 广告 → 收益 → 钱包 ----------------
|
||
_head "链路④ 广告→收益→钱包"
|
||
check "ad/slot/list-enabled(广告位)" GET /app-api/ad/slot/list-enabled envelope
|
||
check "trade/account/mine(钱包)" GET /app-api/trade/account/mine envelope
|
||
check "trade/income/page(收益流水)" GET "/app-api/trade/income/page?pageNo=1&pageSize=5" envelope
|
||
|
||
# ---------------- 链路⑤ 数据回路(遥测→质量分→feed 重排)----------------
|
||
_head "链路⑤ 数据回路(质量分回灌)"
|
||
# 结构断言:feed 首条携带 qualityScore(feed 排序由质量分驱动 = 回路已回灌)
|
||
QSCORE="$(api GET "/app-api/feed/stream?size=1" | sed '$d' | python3 -c "
|
||
import sys,json
|
||
try:
|
||
d=json.load(sys.stdin); lst=(d.get('data') or {}).get('list') or []
|
||
print(lst[0].get('qualityScore') if lst and lst[0].get('qualityScore') is not None else 'NONE')
|
||
except: print('NONE')" 2>/dev/null)"
|
||
if [ "$QSCORE" != "NONE" ] && [ -n "$QSCORE" ]; then PASS=$((PASS+1)); _log " ${G}✓${N} feed 携带 qualityScore=${QSCORE}(质量分回路已回灌)"; else FAIL=$((FAIL+1)); FAILED_ITEMS+=("feed 无 qualityScore"); _log " ${R}✗${N} feed 首条无 qualityScore"; fi
|
||
|
||
# ---------------- --deep:写路径(生成入队 + 遥测事件)----------------
|
||
if [ "$DEEP" = "1" ]; then
|
||
_head "深度(--deep)写路径:创作→生成入队"
|
||
# 真实创作链写路径:create_draft → studio_generate(契约 backend_gw.py:123/130)。
|
||
# generate body = {sessionId: draft.id, prompt}(注意 sessionId=draft 的 id 非 gameId;无 templateId,模板在 draft 时绑定)。
|
||
# 入队会触发真实执行器(poll=10s,约 18-27s 落包),本冒烟**只验入队 200/code=0**,不等生成完成。
|
||
# 不验 telemetry/interact 写:① 遥测回路产出已由链路⑤(feed 携 qualityScore)结构性验证;
|
||
# ② 完整深度 e2e(play→judge→publish)是 agent-loop orchestrator 批跑的职责,不与冒烟门重叠。
|
||
SID="$(api POST /app-api/studio/draft '{"title":"冒烟门-deep-idle","templateId":"idle"}' | sed '$d' | python3 -c "import sys,json
|
||
try: print((json.load(sys.stdin).get('data') or {}).get('id',''))
|
||
except: print('')" 2>/dev/null)"
|
||
if [ -n "$SID" ] && [ "$SID" != "None" ]; then
|
||
check "studio/generate(入队 sessionId=${SID})" POST /app-api/studio/generate envelope \
|
||
"{\"sessionId\":$SID,\"prompt\":\"一个挂机种树攒果实的放置小游戏\"}"
|
||
else FAIL=$((FAIL+1)); FAILED_ITEMS+=("deep: draft 无 id"); _log " ${R}✗${N} deep: draft 未拿到 sessionId(id)"; fi
|
||
fi
|
||
|
||
# ---------------- 总判 ----------------
|
||
_head "总判"
|
||
TOTAL=$((PASS+FAIL))
|
||
if [ "$FAIL" = "0" ]; then
|
||
_log "${G}SMOKE_PASS${N} $PASS/$TOTAL 全过"
|
||
echo "SMOKE_VERDICT=PASS"
|
||
exit 0
|
||
else
|
||
_log "${R}SMOKE_FAIL${N} $PASS/$TOTAL 过,$FAIL 项 FAIL:"
|
||
for it in "${FAILED_ITEMS[@]}"; do _log " ${R}-${N} $it"; done
|
||
echo "SMOKE_VERDICT=FAIL"
|
||
exit 1
|
||
fi
|